npm — Publishing Packages
Publishing a package to the npm registry makes it available to millions of developers worldwide. Whether you are sharing a reusable utility, a UI component library, or a CLI tool, understanding the publishing workflow is essential.
This guide covers preparing a package for publishing, scoped packages, access control, two-factor authentication, CI/CD publishing, and semantic-release automation.
Before publishing, ensure your package.json has all the required fields and your package is properly configured.
| 1 | // Essential package.json fields for publishing: |
| 2 | { |
| 3 | "name": "my-utility", |
| 4 | "version": "1.0.0", |
| 5 | "description": "A useful utility for doing things", |
| 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 | }, |
| 16 | "files": ["dist"], |
| 17 | "license": "MIT", |
| 18 | "keywords": ["utility", "helpers"], |
| 19 | "author": "Your Name <email@example.com>", |
| 20 | "repository": { |
| 21 | "type": "git", |
| 22 | "url": "git+https://github.com/username/my-utility.git" |
| 23 | }, |
| 24 | "bugs": { |
| 25 | "url": "https://github.com/username/my-utility/issues" |
| 26 | }, |
| 27 | "homepage": "https://github.com/username/my-utility#readme", |
| 28 | "engines": { |
| 29 | "node": ">=18.0.0" |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | // Field descriptions: |
| 34 | // name: Package name (must be unique on registry) |
| 35 | // version: Current version (SemVer) |
| 36 | // main: Entry point for CommonJS |
| 37 | // module: Entry point for ES modules |
| 38 | // types: TypeScript declaration file |
| 39 | // exports: Modern entry points (Node.js 12+) |
| 40 | // files: Which files to include in the tarball |
| 41 | // license: SPDX license identifier |
info
Scoped packages are namespaced with an @ prefix, such as @my-org/my-package. Scopes are associated with npm user or organization accounts.
| 1 | // Scoped package example: |
| 2 | { |
| 3 | "name": "@my-org/utility-lib", |
| 4 | "version": "1.0.0", |
| 5 | "publishConfig": { |
| 6 | "access": "public" |
| 7 | } |
| 8 | } |
| 9 | |
| 10 | // Scopes serve multiple purposes: |
| 11 | // - Namespace packages under an organization |
| 12 | // - Prevent naming conflicts |
| 13 | // - Enable private package management |
| 14 | // - Group related packages logically |
| 15 | |
| 16 | // Publishing scoped packages: |
| 17 | # By default, scoped packages are PRIVATE |
| 18 | # To publish a public scoped package: |
| 19 | npm publish --access public |
| 20 | |
| 21 | # Or configure once in package.json: |
| 22 | # "publishConfig": { "access": "public" } |
| 23 | # Then just: npm publish |
| 24 | |
| 25 | // Installing scoped packages: |
| 26 | npm install @my-org/utility-lib |
| 27 | import { something } from "@my-org/utility-lib" |
warning
Control which files are included in the published tarball using .npmignore or the files field in package.json. This keeps your package small and avoids publishing sensitive files.
| 1 | # .npmignore — exclude files from package |
| 2 | # If .npmignore exists, it overrides .gitignore |
| 3 | |
| 4 | node_modules/ |
| 5 | src/ |
| 6 | tests/ |
| 7 | .git/ |
| 8 | .github/ |
| 9 | *.map |
| 10 | *.ts (if you publish compiled JS only) |
| 11 | .env |
| 12 | .DS_Store |
| 13 | coverage/ |
| 14 | |
| 15 | # If you don't have .npmignore, npm uses .gitignore |
| 16 | # Plus these are ALWAYS excluded: |
| 17 | # .git, CVS, .svn, .hg, .lock-wscript, .wafpickle-*, |
| 18 | # config.gypi, CVS, .svn, .DS_Store |
| 19 | |
| 20 | # These are ALWAYS included (even with .npmignore): |
| 21 | # package.json, README, LICENSE, CHANGELOG |
| 22 | # The "main" file and its dependencies |
| 1 | // Better approach: use "files" field (whitelist) |
| 2 | { |
| 3 | "files": [ |
| 4 | "dist", |
| 5 | "README.md", |
| 6 | "LICENSE", |
| 7 | "CHANGELOG.md" |
| 8 | ] |
| 9 | } |
| 10 | |
| 11 | // With the "files" field, npm ONLY includes: |
| 12 | // 1. Files/directories listed in "files" |
| 13 | // 2. package.json, README, LICENSE (always) |
| 14 | // 3. The main/module entry point |
| 15 | |
| 16 | // Check what will be published: |
| 17 | npm pack --dry-run |
| 18 | # Lists all files that will be in the tarball |
| 19 | |
| 20 | # Check package size: |
| 21 | npm pack --dry-run | wc -c |
| 22 | # Or use: npx pkg-size |
| 23 | |
| 24 | // Size matters for install speed: |
| 25 | // - Keep packages under 100KB if possible |
| 26 | // - Avoid bundling unnecessary files |
| 27 | // - Use .npmignore to exclude tests, docs |
best practice
The npm publish command uploads your package to the registry. Several flags and configurations control the publishing behavior.
| 1 | # Basic publish |
| 2 | npm publish |
| 3 | |
| 4 | # Publish with a specific tag |
| 5 | npm publish --tag beta |
| 6 | # Install with: npm install my-package@beta |
| 7 | |
| 8 | # Publish scoped package as public |
| 9 | npm publish --access public |
| 10 | |
| 11 | # Dry run (don't actually publish) |
| 12 | npm publish --dry-run |
| 13 | |
| 14 | # Publish from a specific directory |
| 15 | npm publish ./dist |
| 16 | |
| 17 | # Publish with OTP (2FA code) |
| 18 | npm publish --otp 123456 |
| 19 | |
| 20 | # Initialize package before publish: |
| 21 | npm init |
| 22 | # Creates package.json interactively |
| 23 | |
| 24 | # Login before publishing: |
| 25 | npm login |
| 26 | # Username: your-username |
| 27 | # Password: ******** |
| 28 | # Email: your-email@example.com |
| 29 | # Enter OTP: 123456 |
| 30 | |
| 31 | # Who is logged in? |
| 32 | npm whoami |
| 33 | |
| 34 | # Check if package name is available: |
| 35 | npm view my-package-name |
| 36 | # 404 if available, shows info if taken |
info
npm supports both public and private packages. Private packages require a paid npm account and allow you to restrict access to specific collaborators or teams.
| 1 | # Set package access: |
| 2 | # Public (visible to everyone): |
| 3 | npm publish --access public |
| 4 | |
| 5 | # Private (requires paid npm org account): |
| 6 | npm publish --access restricted |
| 7 | |
| 8 | # Change access after publishing: |
| 9 | npm access public @my-org/my-package |
| 10 | npm access restricted @my-org/my-package |
| 11 | |
| 12 | # Grant access to collaborators: |
| 13 | npm access grant read-write user:collab-user @my-org/my-package |
| 14 | npm access grant read-only team:@my-org/developers @my-org/my-package |
| 15 | |
| 16 | # Revoke access: |
| 17 | npm access revoke user:collab-user @my-org/my-package |
| 18 | |
| 19 | # View package access: |
| 20 | npm access ls-collaborators @my-org/my-package |
| 21 | npm access ls-packages |
| 22 | |
| 23 | # Two-Factor Authentication (2FA): |
| 24 | # Require 2FA for publishing: |
| 25 | npm profile enable-2fa auth-and-writes |
| 26 | # Or: npm profile enable-2fa auth-only (login only) |
| 27 | |
| 28 | # Set package to require 2FA for contributors: |
| 29 | npm access set-mfa write-required @my-org/my-package |
Two-factor authentication (2FA) adds a critical security layer to your npm account. It prevents unauthorized publishing even if your password is compromised.
| 1 | # Enable 2FA on npm |
| 2 | npm profile enable-2fa auth-and-writes |
| 3 | # You'll be prompted to scan a QR code with your authenticator app |
| 4 | |
| 5 | # 2FA modes: |
| 6 | # auth-only: 2FA for login only |
| 7 | # auth-and-writes: 2FA for login AND publishing |
| 8 | |
| 9 | # Publishing with 2FA: |
| 10 | npm publish --otp 123456 |
| 11 | # Or use an OTP device: |
| 12 | npm publish |
| 13 | |
| 14 | # If you don't pass --otp, npm prompts for it |
| 15 | |
| 16 | # Configure npm for automated publishing (CI): |
| 17 | # Create an automation token (not your password): |
| 18 | npm token create --read-only |
| 19 | npm token create --publish # for CI/CD |
| 20 | |
| 21 | # List tokens: |
| 22 | npm token list |
| 23 | |
| 24 | # Revoke a token: |
| 25 | npm token revoke <token-id> |
| 26 | |
| 27 | # Automation tokens bypass 2FA for CI/CD |
| 28 | # Store tokens as secrets in your CI provider: |
| 29 | # GitHub Actions: secrets.NPM_TOKEN |
warning
Consistent versioning and changelog generation help users understand what changed between releases. Follow SemVer strictly and maintain a changelog.
| 1 | # Bump version and create git tag: |
| 2 | npm version patch # 1.0.0 -> 1.0.1 |
| 3 | npm version minor # 1.0.0 -> 1.1.0 |
| 4 | npm version major # 1.0.0 -> 2.0.0 |
| 5 | |
| 6 | # Each creates a git commit and tag: |
| 7 | # git add package.json && git commit -m "1.0.1" |
| 8 | # git tag v1.0.1 |
| 9 | |
| 10 | # With a pre-release identifier: |
| 11 | npm version prepatch --preid=alpha |
| 12 | # 1.0.0 -> 1.0.1-alpha.0 |
| 13 | |
| 14 | # Changelog with standard-version: |
| 15 | npx standard-version |
| 16 | # Automatically generates CHANGELOG.md from |
| 17 | # Conventional Commits, bumps version, and tags |
| 18 | |
| 19 | # Or generate changelog manually: |
| 20 | git log --oneline --no-merges v1.0.0..HEAD |
semantic-release fully automates the package release workflow — version bumping, changelog generation, git tagging, and npm publishing — based on Conventional Commits.
| 1 | // Install semantic-release |
| 2 | // package.json configuration: |
| 3 | { |
| 4 | "scripts": { |
| 5 | "semantic-release": "semantic-release" |
| 6 | } |
| 7 | } |
| 8 | |
| 9 | // .releaserc.json: |
| 10 | { |
| 11 | "branches": ["main"], |
| 12 | "plugins": [ |
| 13 | "@semantic-release/commit-analyzer", |
| 14 | "@semantic-release/release-notes-generator", |
| 15 | "@semantic-release/changelog", |
| 16 | "@semantic-release/npm", |
| 17 | "@semantic-release/github" |
| 18 | ] |
| 19 | } |
| 20 | |
| 21 | // Commit messages determine version bump: |
| 22 | // fix: -> PATCH (1.0.0 -> 1.0.1) |
| 23 | // feat: -> MINOR (1.0.0 -> 1.1.0) |
| 24 | // feat!: or BREAKING CHANGE: -> MAJOR (1.0.0 -> 2.0.0) |
| 25 | |
| 26 | // GitHub Actions workflow: |
| 27 | # .github/workflows/release.yml |
| 28 | name: Release |
| 29 | on: |
| 30 | push: |
| 31 | branches: [main] |
| 32 | jobs: |
| 33 | release: |
| 34 | runs-on: ubuntu-latest |
| 35 | steps: |
| 36 | - uses: actions/checkout@v4 |
| 37 | - uses: actions/setup-node@v4 |
| 38 | with: |
| 39 | node-version: 20 |
| 40 | - run: npm ci |
| 41 | - run: npm run build |
| 42 | - run: npx semantic-release |
| 43 | env: |
| 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} |
pro tip
Automating package publishing in CI/CD ensures consistency and reduces manual errors. GitHub Actions is the most common CI provider for npm publishing.
| 1 | # GitHub Actions — manual publish workflow |
| 2 | # .github/workflows/publish.yml |
| 3 | name: Publish to npm |
| 4 | on: |
| 5 | release: |
| 6 | types: [published] |
| 7 | jobs: |
| 8 | publish: |
| 9 | runs-on: ubuntu-latest |
| 10 | steps: |
| 11 | - uses: actions/checkout@v4 |
| 12 | - uses: actions/setup-node@v4 |
| 13 | with: |
| 14 | node-version: 20 |
| 15 | registry-url: https://registry.npmjs.org |
| 16 | - run: npm ci |
| 17 | - run: npm test |
| 18 | - run: npm run build |
| 19 | - run: npm publish |
| 20 | env: |
| 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} |
| 22 | |
| 23 | # For scoped packages with npm provenance: |
| 24 | # (requires npm 9+, Node.js 16+) |
| 25 | # npm publish --provenance |
| 26 | # Proves the package was built on GitHub Actions |
| 27 | # and links to the exact workflow run |
info
Test Before Publishing
Use a prepublishOnly script to run tests and linting before every publish. This prevents publishing broken packages.
Use semantic-release for Automation
Automate versioning, changelog generation, and publishing with semantic-release. It enforces Conventional Commits and eliminates manual release errors.
Enable 2FA for Publishing
Require two-factor authentication for publishing to your packages. Use automation tokens for CI/CD to bypass 2FA in automated workflows.
Keep Packages Small
Use the files field to publish only what is necessary. Run npm pack --dry-run to verify. Smaller packages install faster and are less likely to contain sensitive files.
Use Provenance for Open Source
Publish with --provenance to link your package to the exact CI build that produced it. This helps consumers verify authenticity.