Back to Blog
npmPackagesJavaScriptTypeScript

Building and Publishing npm Packages

Create professional npm packages. From project setup to testing to publishing and versioning.

B
Bootspring Team
Engineering
June 20, 2022
5 min read

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-package
1// 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 41MIT

Versioning 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 publish

Publishing#

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-run
1# .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.

Share this article

Help spread the word about Bootspring