Skip to main content

Publishing an npm Package: Complete Guide 2026

·PkgPulse Team
0

TL;DR

Publishing an npm package in 2026 requires more than npm publish. Classic npm tokens are deprecated — you need Granular Access Tokens. The --provenance flag cryptographically links your package to its source commit. Dual CJS/ESM output is expected by consumers. Changesets automates the versioning and publishing workflow. This guide covers the complete, modern workflow from zero to published.

Key Takeaways

  • Classic npm "Automation" tokens are deprecated — use Granular Access Tokens with the minimum required scope
  • npm publish --provenance requires running in a GitHub Actions (or similar) CI environment
  • The exports field in package.json controls what consumers can import — required for proper ESM/CJS dual publishing
  • Scoped packages (@myorg/package) are private by default — use --access public to publish publicly
  • Changesets is the community standard for monorepo publishing workflows
  • TypeScript source should be compiled to both ESM (.mjs) and CJS (.cjs) for maximum compatibility

Step 1: Set Up Your Package

package.json Essentials

{
  "name": "@myorg/my-package",
  "version": "0.1.0",
  "description": "What this package does",
  "license": "MIT",
  "author": "Your Name <email@example.com>",
  "repository": {
    "type": "git",
    "url": "https://github.com/myorg/my-package.git"
  },
  "keywords": ["relevant", "keywords", "here"],
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    }
  },
  "files": ["dist", "README.md", "CHANGELOG.md"],
  "scripts": {
    "build": "tsc && rollup -c",
    "test": "vitest",
    "prepublishOnly": "npm run build && npm test"
  }
}

Key fields explained:

  • exports: The modern way to control what can be imported. Takes precedence over main. Define import for ESM consumers, require for CJS consumers.
  • files: Only these files are included in the published package. Keep it small.
  • prepublishOnly: Runs before npm publish — ensure tests pass and the build is fresh.

TypeScript Configuration for Dual Output

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src/**/*"],
  "exclude": ["**/*.test.ts", "dist"]
}

For dual ESM/CJS output, use a bundler (Rollup, tsup, or unbuild) that generates both formats:

# tsup is the simplest option
npx tsup src/index.ts --format cjs,esm --dts

Step 2: Create Granular Access Tokens

Classic npm tokens have been deprecated in favor of Granular Access Tokens, which let you specify exactly which packages and operations the token can access.

Creating a Granular Access Token:

  1. Go to npmjs.com → Account Settings → Access Tokens → Generate New Token → Granular Access Token
  2. Set expiration (90 days recommended for CI tokens)
  3. Packages: choose "Only select packages and scopes" — limit to just your package
  4. Permissions: Read and write
  5. Copy the token — it's shown only once

For CI/CD (GitHub Actions): Add the token as NPM_TOKEN in your repository's GitHub Secrets (Settings → Secrets and variables → Actions).

Never put npm tokens in your codebase or .npmrc files committed to git.


Step 3: npm Provenance Signing

Provenance creates a cryptographic link between your published package and the specific commit and CI run that produced it. It's built on the Sigstore standard.

# .github/workflows/publish.yml
name: Publish to npm
on:
  push:
    tags: ['v*']
permissions:
  contents: read
  id-token: write  # Required for provenance
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci
      - run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

The id-token: write permission is required — GitHub Actions generates an OIDC token that Sigstore uses to create the attestation.

After publishing, your package page on npmjs.com shows a "Provenance" badge with links to the source commit and CI run. This gives package consumers confidence that the published artifact hasn't been tampered with between your source code and the npm registry.


Step 4: Semantic Versioning and Changesets

Before publishing, understand semantic versioning rules:

  • PATCH (1.0.x): Bug fixes, no API changes
  • MINOR (1.x.0): New features, backwards compatible
  • MAJOR (x.0.0): Breaking changes

For standalone packages, use npm version:

npm version patch  # bumps 1.0.0 → 1.0.1
npm version minor  # bumps 1.0.0 → 1.1.0
npm version major  # bumps 1.0.0 → 2.0.0

For monorepos, use Changesets:

npx changeset  # Describe what changed
npx changeset version  # Bump versions
npx changeset publish  # Publish to npm

Changesets coordinates versioning across multiple packages, generates changelogs, and handles the dependency chain — if you bump @myorg/utils, packages that depend on it get their own version bumps automatically.

For the full monorepo workflow including Turborepo and pnpm workspaces integration with Changesets, see Dependency Management Strategy for Monorepos 2026.


Step 5: Pre-Publish Checklist

Before every publish:

Package quality:

  • README explains what the package does, how to install it, and basic usage
  • CHANGELOG.md is updated (Changesets does this automatically)
  • All tests pass
  • TypeScript types are exported correctly
  • npm pack --dry-run confirms only intended files are included

Security:

  • No .env files, credentials, or sensitive data in files
  • npm audit passes (or you've accepted/addressed findings)
  • Dependencies are minimal — only what's actually needed

Compatibility:

  • Package works in both Node.js ESM and CJS environments
  • Minimum Node.js version is specified in engines
  • Browser compatibility tested if it's a browser package

npm publish dry run:

npm publish --dry-run
# Shows what would be published without actually publishing

Step 6: Publishing

For a standalone package:

npm publish --access public  # For scoped packages; unscoped are public by default

For Changesets in CI:

- name: Publish
  run: pnpm changeset publish
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
    NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Post-Publish: Deprecation and Unpublishing

Deprecating a version (marks as deprecated without removing):

npm deprecate my-package@1.0.0 "This version has a security vulnerability, upgrade to 1.1.0"

Unpublishing (permanent removal — use carefully):

# Within 72 hours of publishing
npm unpublish my-package@1.0.0

# After 72 hours, requires npm support intervention

Unpublishing is almost always wrong. It breaks everyone depending on that version. Prefer deprecation + a patch release that fixes the issue.


Package Documentation That Drives Adoption

A well-structured README is the difference between a package that gets adopted and one that doesn't. npm downloads the README and displays it on the package page — it's your primary marketing channel.

Essential README Sections

1. What problem does it solve? (3-5 sentences max, no jargon)

2. Installation:

npm install your-package
# or
pnpm add your-package

3. Quick start: A working code example that shows the core use case. Copy-pasteable, demonstrates real value immediately.

4. API reference: A table or list of all exported functions/classes with signatures and descriptions. TypeScript users will check this before looking at the types.

5. Configuration: If the package accepts options, document every option with type, default, and description.

6. Changelog link: Link to your CHANGELOG.md or GitHub releases — users upgrading from an older version need to know what changed.

TypeScript Type Quality

Well-typed packages don't just work with TypeScript — they make the developer experience dramatically better. Considerations:

  • Export all types that consumers might need to use: parameter types, return types, option objects
  • Don't export implementation details (private types, internal utility types)
  • Use JSDoc comments on exported symbols — TypeScript surfaces these in editor intellisense
  • Test your types with dtslint or tsd to catch regressions
// Export the options interface so consumers can type their config objects
export interface ResolverOptions {
  timeout?: number;
  retries?: number;
  baseUrl: string;
}

export function resolve(url: string, options: ResolverOptions): Promise<Response>;

If a consumer needs to write ResolverOptions in their own code, they should be able to import it from your package rather than redefining it.


Package Size Considerations

Package size affects install time, bundle size, and the overall DX of depending on your package. A 50KB utility shouldn't pull in 5MB of dependencies.

Check your package size before publishing:

# See what will be published and at what sizes
npm pack --dry-run

# Or check with bundlephobia (paste the package name)
# https://bundlephobia.com

Rules for minimal package size:

  • Put heavy dependencies in peerDependencies, not dependencies, if the consuming project likely already has them (React, TypeScript, etc.)
  • Use devDependencies for build tools — they shouldn't be installed by consumers
  • Audit your dependencies list — is every dependency actually needed at runtime?
  • Consider inline small utilities rather than depending on single-function packages
  • Use tree-shakeable exports (named exports, not default export of a big object)

A package that adds 500KB to a consumer's bundle will be avoided. A package that adds 2KB will be included without second thought.


Common Publishing Mistakes

Mistake: Publishing without building The prepublishOnly script prevents this, but only if you use npm publish. Double-check your build output is current before manual publishes.

Mistake: Wrong package name Package names are permanent. Once published under my-paackage, you can't rename it — you have to publish as my-package (different package) and deprecate the typo.

Mistake: Including development files Without a careful files field, your package might include test files, source maps, or documentation — adding unnecessary size.

Mistake: Breaking changes in patch/minor versions Clearly document breaking changes in a MAJOR version. Consumers use ^1.0.0 expecting that installing updates won't break their code.

For the semantic versioning rules and automation patterns, see Semantic Versioning: Breaking Changes Guide 2026.


Distribution Strategy: CJS vs ESM vs Dual

The JavaScript module format question affects every library author in 2026. Here's the current state and the recommended approach.

The problem: npm packages need to work in multiple environments — Node.js with CommonJS require(), Node.js with ESM import, browser bundlers that use tree-shaking, and TypeScript codebases. Each environment has different format preferences.

What each format is good for:

CommonJS (.cjs): Required for older Node.js codebases, some build tools, and packages that use __dirname/__filename. The .cjs extension (or "main" field pointing to CJS output) tells Node.js to treat this as CommonJS.

ESM (.mjs or .js with "type": "module"): Required for tree-shaking in bundlers like Webpack and Rollup. Allows top-level await. The future of JavaScript modules. ESM files can import CJS packages but CJS files cannot require() ESM packages (without dynamic import()).

The 2026 recommendation — dual CJS/ESM output:

Publish both formats to maximize compatibility. This is what virtually all popular npm packages do today.

The exports field controls which format consumers get:

{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",    // ESM consumers
      "require": "./dist/index.cjs",   // CJS consumers
      "types": "./dist/index.d.ts"     // TypeScript
    }
  }
}

Bundlers like tsup, unbuild, and pkgroll generate both formats from a single TypeScript source. The build step is typically 1-3 seconds.

When to ship ESM-only:

If your package is only intended for modern environments (browser-native, Deno, or Node.js 22+ where require(ESM) is supported), you can ship ESM-only. This simplifies your build. But you'll exclude older Node.js users and some legacy build toolchains. For a new package with no existing user base, ESM-only is increasingly viable in 2026.


Package Naming and Discoverability

The name of your npm package is the first and most lasting decision you make as a package author. Unlike almost everything else in software, you cannot rename a published package — the name is permanent on the registry, and any rename means publishing under a new name and deprecating the old one, fragmenting your install base and breaking anyone who doesn't update. This makes the naming decision worth spending real time on before that first publish.

The scoped package convention — @your-org/package-name rather than a flat package-name — is the right choice for organizational packages for several reasons beyond organizational clarity. Scoped packages occupy their own namespace on npm, which means you can publish @yourorg/utils even if utils as a flat package is taken by something else entirely. More importantly, scoped packages dramatically reduce the typosquatting risk that affects popular flat-named packages. An attacker can register lodash variants as flat package names but cannot register @lodash/ scoped packages without owning that scope. For packages you're publishing as part of a company or team, the scope also signals provenance — users seeing @stripe/ know that package is from Stripe, not a third party.

Naming conventions within the package name itself should prioritize clarity over cleverness. The standard is kebab-case (my-package-name), never underscores or camelCase. The name should describe what the package does, ideally including the problem domain: form-validator, react-data-table, markdown-parser. Generic names that conflict with popular packages cause confusion and SEO problems on the registry. A package named utils tells consumers nothing about its purpose. A package named date-range-utils is specific enough to be findable and distinctive enough to avoid conflicts.

npm's search ranking is not purely based on download counts. Several factors influence where a package appears in search results, and understanding them helps new packages get discovered. The keyword field in package.json is indexed by npm's search engine — packages with well-chosen keywords rank higher for relevant searches. The sweet spot is five to ten keywords that accurately describe the package's domain, use case, and related technology. Overstuffing keywords with every loosely related term is counterproductive: npm's algorithm penalizes keyword spam, and it creates a poor signal-to-noise ratio for consumers evaluating your package. The description field matters too — it should be a single clear sentence describing what the package does and for whom, not a marketing tagline.

The README is a genuine ranking signal and not just a documentation nicety. npm's registry gives search ranking boosts to packages with comprehensive READMEs, and for good reason: a README-complete package is a signal of a maintained, intentional package rather than an experiment or stub. Beyond search ranking, the README on npmjs.com is your package's primary interface with potential adopters — they arrive at your package page before they look at source code, before they read your changelog, before they decide whether to try it. A README with clear installation instructions, a working code example, and an API reference is the difference between a developer spending thirty more seconds evaluating your package or moving on to the next search result.


Versioning and Changelogs as a Trust Signal

Trust is the currency of the npm ecosystem, and versioning discipline is one of the most visible ways package authors build or destroy it. Developers who depend on your package are making a bet that future versions of it won't silently break their code. The mechanism that governs that bet is semantic versioning, and how faithfully you honor it determines whether developers pin your package out of mistrust, eagerly apply updates, or recommend it to colleagues.

The core contract is straightforward: patch versions contain only bug fixes and no behavior changes, minor versions add new features while maintaining backward compatibility, and major versions introduce breaking changes. Where packages earn poor reputations is in breaking semver — shipping a behavioral change in a minor or patch release, or deprecating a function in a minor release and removing it in the next patch. The consequences compound over time. Developers who get burned by a semver violation start pinning your package to exact versions, which means they stop receiving security patches. They add it to internal blocklists. They recommend alternatives. Violating semver is not just a version numbering mistake; it's breaking a contract with everyone in your user base.

The changelog is the artifact that makes versioning trust visible. A CHANGELOG.md that accurately describes what changed in each release allows developers to make informed decisions about updates: they can read the changelog for 2.3.0 before upgrading from 2.2.1 and know what they're getting. The Keep a Changelog standard (keepachangelog.com) provides a consistent structure that developers recognize: each release gets a section with subsections for Added, Changed, Deprecated, Removed, Fixed, and Security changes. The quality bar is "what changed and why," not "bug fixes and improvements." The latter tells a developer nothing useful; the former tells them exactly what to test after upgrading.

Generating changelogs from conventional commits eliminates the work of maintaining them manually. Conventional commits format (feat: add X, fix: resolve Y, BREAKING CHANGE: removed Z) provides enough structure for automated tools like conventional-changelog or release-please to generate accurate release notes from your commit history. This creates a natural link between how you write commit messages during development and the quality of the changelog your users read. release-please, Google's release automation tool, goes further: it opens automated PRs to update versions and changelogs based on your conventional commit history, removing the manual version bump step entirely.

The release cadence question — many small releases versus batched larger ones — does not have a universal answer, but the considerations are worth understanding. Frequent small releases reduce the risk associated with any single update; if something breaks, you have fewer changes to examine. They also get bug fixes and security patches to users faster. Batched releases are easier for consumers to track and feel less disruptive. For security patches specifically, the answer should always be to ship immediately regardless of cadence.

Pre-release versions — alpha, beta, and release candidate — are underused by many package authors and useful in ways that go beyond just "things might break." A beta channel gives power users and integration partners the ability to test your API design before it's locked into a stable release. If the API is wrong, you find out before committing to it in semver. Alpha versions signal genuine instability and invite feedback on direction rather than just implementation. Release candidates signal "this is what we plan to ship, please find problems." Using these versions deliberately, rather than going directly from development to stable, gives your package a more professional trajectory and builds the kind of community investment that makes adoption self-sustaining.


Methodology

This article draws on:

  • npm documentation: Granular Access Tokens, provenance publishing, package.json exports
  • Sigstore/npm provenance specification
  • Changesets documentation and GitHub Actions integration guides
  • TypeScript project configuration documentation
  • Total TypeScript guide on npm package publishing
  • tsup documentation for dual CJS/ESM output
  • npm supply chain security practices: see npm Supply Chain Security Guide 2026 for provenance verification context

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.