Creating npm packages lets you share code across projects. Here's how to build professional packages.
Project Setup#
1# Initialize package
2mkdir my-package && cd my-package
3npm init -y
4
5# Or use create templates
6npx create-tsup my-package1// package.json
2{
3 "name": "@myorg/utils",
4 "version": "1.0.0",
5 "description": "Utility functions",
6 "main": "./dist/index.js",
7 "module": "./dist/index.mjs",
8 "types": "./dist/index.d.ts",
9 "exports": {
10 ".": {
11 "import": "./dist/index.mjs",
12 "require": "./dist/index.js",
13 "types": "./dist/index.d.ts"
14 },
15 "./math": {
16 "import": "./dist/math.mjs",
17 "require": "./dist/math.js",
18 "types": "./dist/math.d.ts"
19 }
20 },
21 "files": [
22 "dist"
23 ],
24 "scripts": {
25 "build": "tsup",
26 "dev": "tsup --watch",
27 "test": "vitest",
28 "lint": "eslint src",
29 "prepublishOnly": "npm run build"
30 },
31 "keywords": ["utils", "helpers"],
32 "author": "Your Name",
33 "license": "MIT",
34 "repository": {
35 "type": "git",
36 "url": "https://github.com/myorg/utils"
37 },
38 "devDependencies": {
39 "tsup": "^8.0.0",
40 "typescript": "^5.0.0",
41 "vitest": "^1.0.0"
42 }
43}TypeScript Configuration#
1// tsconfig.json
2{
3 "compilerOptions": {
4 "target": "ES2020",
5 "module": "ESNext",
6 "moduleResolution": "bundler",
7 "lib": ["ES2020"],
8 "strict": true,
9 "declaration": true,
10 "declarationMap": true,
11 "sourceMap": true,
12 "outDir": "dist",
13 "esModuleInterop": true,
14 "skipLibCheck": true,
15 "forceConsistentCasingInFileNames": true,
16 "resolveJsonModule": true,
17 "isolatedModules": true
18 },
19 "include": ["src"],
20 "exclude": ["node_modules", "dist", "**/*.test.ts"]
21}Build Configuration#
1// tsup.config.ts
2import { defineConfig } from 'tsup';
3
4export default defineConfig({
5 entry: ['src/index.ts', 'src/math.ts'],
6 format: ['cjs', 'esm'],
7 dts: true,
8 splitting: false,
9 sourcemap: true,
10 clean: true,
11 minify: false,
12 treeshake: true,
13 external: ['react'], // Don't bundle peer deps
14});Source Code Structure#
1// src/index.ts
2export { formatDate, parseDate } from './date';
3export { debounce, throttle } from './timing';
4export { deepClone, merge } from './objects';
5
6export type { DateFormat, MergeOptions } from './types';
7
8// src/date.ts
9export function formatDate(date: Date, format: string): string {
10 // Implementation
11}
12
13export function parseDate(str: string, format: string): Date {
14 // Implementation
15}
16
17// src/types.ts
18export interface DateFormat {
19 year: 'numeric' | '2-digit';
20 month: 'numeric' | '2-digit' | 'long' | 'short';
21 day: 'numeric' | '2-digit';
22}
23
24export interface MergeOptions {
25 deep?: boolean;
26 arrays?: 'replace' | 'concat';
27}Testing#
1// src/date.test.ts
2import { describe, it, expect } from 'vitest';
3import { formatDate, parseDate } from './date';
4
5describe('formatDate', () => {
6 it('formats date with default format', () => {
7 const date = new Date('2024-01-15');
8 expect(formatDate(date, 'YYYY-MM-DD')).toBe('2024-01-15');
9 });
10
11 it('handles different formats', () => {
12 const date = new Date('2024-01-15');
13 expect(formatDate(date, 'MM/DD/YYYY')).toBe('01/15/2024');
14 });
15});
16
17describe('parseDate', () => {
18 it('parses date string', () => {
19 const result = parseDate('2024-01-15', 'YYYY-MM-DD');
20 expect(result.getFullYear()).toBe(2024);
21 expect(result.getMonth()).toBe(0);
22 expect(result.getDate()).toBe(15);
23 });
24});1// vitest.config.ts
2import { defineConfig } from 'vitest/config';
3
4export default defineConfig({
5 test: {
6 globals: true,
7 coverage: {
8 provider: 'v8',
9 reporter: ['text', 'html'],
10 exclude: ['**/*.test.ts', '**/types.ts'],
11 },
12 },
13});Documentation#
1<!-- README.md -->
2# @myorg/utils
3
4Utility functions for modern JavaScript applications.
5
6## Installation
7
8\`\`\`bash
9npm install @myorg/utils
10\`\`\`
11
12## Usage
13
14\`\`\`typescript
15import { formatDate, debounce } from '@myorg/utils';
16
17// Format dates
18const formatted = formatDate(new Date(), 'YYYY-MM-DD');
19
20// Debounce functions
21const debouncedSearch = debounce((query) => {
22 search(query);
23}, 300);
24\`\`\`
25
26## API Reference
27
28### formatDate(date, format)
29
30Formats a date according to the specified format string.
31
32| Param | Type | Description |
33|-------|------|-------------|
34| date | `Date` | The date to format |
35| format | `string` | Format pattern |
36
37**Returns:** `string`
38
39## License
40
41MITVersioning with Changesets#
1// .changeset/config.json
2{
3 "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
4 "changelog": "@changesets/cli/changelog",
5 "commit": false,
6 "fixed": [],
7 "linked": [],
8 "access": "public",
9 "baseBranch": "main",
10 "updateInternalDependencies": "patch"
11}1# Create changeset
2npx changeset
3
4# Version packages
5npx changeset version
6
7# Publish
8npx changeset publishPublishing#
1# Login to npm
2npm login
3
4# Publish public package
5npm publish --access public
6
7# Publish scoped package
8npm publish --access public
9
10# Publish beta version
11npm publish --tag beta
12
13# Dry run (test without publishing)
14npm publish --dry-run1# .github/workflows/release.yml
2name: Release
3
4on:
5 push:
6 branches: [main]
7
8jobs:
9 release:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v4
13
14 - uses: actions/setup-node@v4
15 with:
16 node-version: 20
17 registry-url: 'https://registry.npmjs.org'
18
19 - run: npm ci
20 - run: npm test
21 - run: npm run build
22
23 - name: Create Release PR or Publish
24 uses: changesets/action@v1
25 with:
26 publish: npx changeset publish
27 env:
28 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 NPM_TOKEN: ${{ secrets.NPM_TOKEN }}Package.json Best Practices#
1{
2 "name": "@myorg/utils",
3 "version": "1.0.0",
4
5 // Entry points
6 "main": "./dist/index.js",
7 "module": "./dist/index.mjs",
8 "types": "./dist/index.d.ts",
9
10 // Modern exports field
11 "exports": {
12 ".": {
13 "import": {
14 "types": "./dist/index.d.mts",
15 "default": "./dist/index.mjs"
16 },
17 "require": {
18 "types": "./dist/index.d.ts",
19 "default": "./dist/index.js"
20 }
21 }
22 },
23
24 // Only include dist in package
25 "files": ["dist"],
26
27 // Side effects for tree shaking
28 "sideEffects": false,
29
30 // Engines
31 "engines": {
32 "node": ">=18"
33 },
34
35 // Peer dependencies (if needed)
36 "peerDependencies": {
37 "react": ">=18"
38 },
39 "peerDependenciesMeta": {
40 "react": {
41 "optional": true
42 }
43 }
44}Best Practices#
Structure:
✓ Clear entry points
✓ Proper exports field
✓ TypeScript declarations
✓ Minimal dependencies
Quality:
✓ Comprehensive tests
✓ Good documentation
✓ Semantic versioning
✓ Changelog maintained
Publishing:
✓ Automate releases with CI
✓ Use changesets for versioning
✓ Test before publishing
✓ Tag releases properly
Conclusion#
Building npm packages involves proper project setup, TypeScript configuration, testing, and automated publishing. Use modern build tools like tsup, maintain good documentation, and automate releases with changesets for a professional package development workflow.