Skip to main content
Technology & EngineeringPackage Management234 lines

Npm Publishing

Publishing packages to the npm registry with proper configuration, access control, and release automation

Quick Summary33 lines
You are an expert in publishing packages to the npm registry, including configuration, access scoping, automation, and lifecycle management.

## Key Points

- **name**: Must be unique on the registry. Scoped packages use `@scope/package-name`.
- **version**: Must follow SemVer and be incremented before each publish.
- **main / module / exports**: Entry points consumers will resolve.
- **files**: Allowlist of files to include in the tarball (preferred over `.npmignore`).
- **publishConfig**: Override registry, access level, or tag at publish time.
- **repository / license / description**: Metadata displayed on npmjs.com.
- **engines**: Declare minimum Node.js version requirements.
- **type**: Set to `"module"` for ESM-first packages.
- **public**: Anyone can install the package. Default for unscoped packages.
- **restricted**: Only users/teams with explicit access can install. Default for scoped packages on paid orgs.
- `latest` (default): What users get with `npm install <pkg>`.
- `next`, `beta`, `canary`, `rc`: Used for pre-release channels.

## Quick Example

```bash
npm publish --tag beta
npm dist-tag add @myorg/utils@3.0.0-beta.1 beta
npm dist-tag ls @myorg/utils
```

```bash
npm pack --dry-run
# Or inspect the actual tarball
npm pack
tar -tzf myorg-utils-2.1.0.tgz
```
skilldb get package-management-skills/Npm PublishingFull skill: 234 lines
Paste into your CLAUDE.md or agent config

npm Publishing — Package Management

You are an expert in publishing packages to the npm registry, including configuration, access scoping, automation, and lifecycle management.

Core Philosophy

Overview

Publishing to npm involves preparing a package with correct metadata, choosing access levels (public or restricted), managing authentication tokens, and automating the release pipeline. A well-configured publish workflow prevents broken releases, leaked files, and version conflicts.

Core Concepts

package.json Fields That Matter for Publishing

  • name: Must be unique on the registry. Scoped packages use @scope/package-name.
  • version: Must follow SemVer and be incremented before each publish.
  • main / module / exports: Entry points consumers will resolve.
  • files: Allowlist of files to include in the tarball (preferred over .npmignore).
  • publishConfig: Override registry, access level, or tag at publish time.
  • repository / license / description: Metadata displayed on npmjs.com.
  • engines: Declare minimum Node.js version requirements.
  • type: Set to "module" for ESM-first packages.

The exports Field (Package Entry Points)

The exports field provides explicit entry point mapping and encapsulation:

{
  "name": "@myorg/utils",
  "version": "2.1.0",
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    },
    "./helpers": {
      "import": "./dist/helpers.mjs",
      "require": "./dist/helpers.cjs",
      "types": "./dist/helpers.d.ts"
    }
  },
  "files": ["dist"],
  "publishConfig": {
    "access": "public"
  }
}

npm Access Levels

  • public: Anyone can install the package. Default for unscoped packages.
  • restricted: Only users/teams with explicit access can install. Default for scoped packages on paid orgs.

Dist Tags

  • latest (default): What users get with npm install <pkg>.
  • next, beta, canary, rc: Used for pre-release channels.
npm publish --tag beta
npm dist-tag add @myorg/utils@3.0.0-beta.1 beta
npm dist-tag ls @myorg/utils

Implementation Patterns

Basic Publish Workflow

# Authenticate (one-time or CI token)
npm login
# or set NPM_TOKEN in CI

# Verify what will be published
npm pack --dry-run

# Bump version (updates package.json and creates git tag)
npm version patch   # 1.0.0 -> 1.0.1
npm version minor   # 1.0.0 -> 1.1.0
npm version major   # 1.0.0 -> 2.0.0

# Publish
npm publish

# For scoped packages that should be public
npm publish --access public

Controlling Published Files

Use the files field as an allowlist (preferred approach):

{
  "files": [
    "dist",
    "README.md",
    "LICENSE"
  ]
}

Always verify the tarball contents before publishing:

npm pack --dry-run
# Or inspect the actual tarball
npm pack
tar -tzf myorg-utils-2.1.0.tgz

Prepublish Scripts

{
  "scripts": {
    "prepublishOnly": "npm run build && npm test",
    "preversion": "npm test",
    "postversion": "git push && git push --tags"
  }
}

CI-Based Publishing with GitHub Actions

name: Publish
on:
  push:
    tags:
      - 'v*'

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write  # Required for npm provenance
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: https://registry.npmjs.org
      - run: npm ci
      - run: npm test
      - run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

npm Provenance

npm provenance links published packages to their source repo and build, increasing supply chain trust:

npm publish --provenance

Requires: GitHub Actions (or GitLab CI) with id-token: write permission and the package linked to a public repo.

Automating Releases with Changesets

npm install -D @changesets/cli
npx changeset init

Workflow:

  1. Contributors run npx changeset and describe their changes.
  2. CI runs npx changeset version to bump versions and update changelogs.
  3. CI runs npx changeset publish to publish updated packages.
{
  "scripts": {
    "release": "changeset publish"
  }
}

Two-Factor Authentication

Enable 2FA on your npm account for publish operations:

npm profile enable-2fa auth-and-writes

For CI, use granular access tokens with limited permissions and IP allowlists.

Best Practices

  • Always use the files allowlist rather than .npmignore to control what gets published.
  • Run npm pack --dry-run before every publish to verify tarball contents.
  • Use prepublishOnly to build and test automatically before publish.
  • Enable npm provenance for supply chain transparency.
  • Use automation tokens with minimal scope for CI publishing.
  • Publish pre-releases to a dist tag (--tag beta) so they never become latest.
  • Include types in the exports field for TypeScript consumers.
  • Set "sideEffects": false if the package is tree-shakeable.
  • Use npm deprecate instead of unpublishing when retiring a version.
  • Use npm owner ls to audit who has publish access.

Common Pitfalls

  • Publishing without building: Forgetting to run the build step, resulting in missing dist files. Use prepublishOnly to guard against this.
  • Leaking secrets or source: Not using the files field, accidentally including .env, config files, or test fixtures in the tarball.
  • Version conflicts: Running npm publish without incrementing the version, causing a 403 error.
  • Scoped package access: Scoped packages default to restricted. First publish of a public scoped package requires --access public.
  • Broken entry points: Mismatched exports/main/module paths that point to files not included in the tarball.
  • Unpublishing regret: npm only allows unpublishing within 72 hours. After that, versions are permanent. Use npm deprecate instead.
  • Missing types condition: TypeScript users get no type resolution if the types condition is missing from exports.
  • Forgetting --tag for pre-releases: Publishing 1.0.0-beta.1 without --tag beta makes it the latest version that all users will install by default.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add package-management-skills

Get CLI access →