Back

How to Create and Publish an npm Package

How to Create and Publish an npm Package

You’ve installed hundreds of packages with npm install. Now you want to publish your own. The process seems straightforward until you discover outdated tutorials recommending Node 12, CommonJS-only setups, and long-lived tokens stored in CI secrets.

This guide walks you through creating and publishing an npm package using modern, secure practices that will still be correct in 2025. We’ll build a simple utility, configure it properly, and publish it with npm Trusted Publishing.

Key Takeaways

  • Use ESM with proper exports field instead of the legacy main field for modern package resolution
  • Compile TypeScript to JavaScript with declaration files rather than shipping raw TypeScript
  • Enable two-factor authentication and use npm Trusted Publishing via GitHub Actions OIDC for secure automated releases
  • Always verify your package contents with npm pack --dry-run before publishing

Prerequisites

Before starting, ensure you have:

  • Node.js 18 or later installed
  • A GitHub account
  • Basic familiarity with npm as a consumer

Initialize Your ESM-First npm Package Setup

Create a new directory and initialize your project:

mkdir use-toggle && cd use-toggle
npm init -y
git init

We’ll build a simple React hook called useToggle. This example demonstrates TypeScript npm package publishing with proper entry points.

Configure package.json for Modern Publishing

Replace your package.json with a properly configured version:

{
  "name": "@yourusername/use-toggle",
  "version": "0.1.0",
  "description": "A simple React hook for boolean toggle state",
  "keywords": ["react", "hook", "toggle", "state"],
  "license": "MIT",
  "author": "Your Name",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/yourusername/use-toggle.git"
  },
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  },
  "files": ["dist"],
  "engines": {
    "node": ">=18"
  },
  "peerDependencies": {
    "react": ">=18"
  },
  "devDependencies": {
    "typescript": "^5.4.0",
    "react": "^18.0.0",
    "@types/react": "^18.0.0"
  },
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  }
}

Key points for this ESM-first npm package setup:

  • type: "module" declares ESM as default
  • exports replaces the legacy main field—this is how modern Node resolves entry points
  • files controls what gets published (only dist/)
  • engines specifies Node 18+, avoiding legacy compatibility issues

Set Up TypeScript Compilation

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "declaration": true,
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Create src/index.ts:

import { useState, useCallback } from 'react';

export function useToggle(initial = false): [boolean, () => void] {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  return [value, toggle];
}

Run npm run build to compile. You’ll get dist/index.js and dist/index.d.ts—JavaScript plus type declarations, not raw TypeScript.

Secure npm Package Publishing in 2025

Create Your npm Account with 2FA

  1. Register at npmjs.com
  2. Enable two-factor authentication immediately (prefer WebAuthn/passkeys)
  3. All new packages now have 2FA enforcement enabled by default
  4. With the new session-based login model, publishing any package—new or existing—requires 2FA during the session

Your First Manual Publish

npm login now creates a two-hour session token. These tokens:

  • Do not appear in the npm UI
  • Expire automatically after two hours
  • Cannot be reused in CI/CD
  • Always enforce 2FA before publishing

Older tooling that authenticates via the deprecated CouchDB endpoint also receives two-hour session tokens during the transition period.

For your initial release:

npm login
npm publish --access public

The --access public flag is required for scoped packages on free accounts.

Verify your package at https://www.npmjs.com/package/@yourusername/use-toggle.

npm Trusted Publishing with CI

Old tutorials tell you to create an NPM_TOKEN and store it as a repository secret. This approach is now obsolete: classic tokens have been permanently revoked, and long-lived CI tokens are no longer supported.

The modern alternative is npm Trusted Publishing via GitHub Actions OIDC. GitHub proves its identity to npm, and npm issues short-lived credentials automatically—no stored token required.

Configure Trusted Publishing

  1. On npmjs.com, go to your package → Settings → Publishing access
  2. Add a new “Trusted Publisher” with your GitHub repository details

Create the Workflow

Add .github/workflows/publish.yml:

name: Publish

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    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

The --provenance flag adds cryptographic attestation linking your package to its source repository—a supply chain security feature npm introduced in 2023.

If OIDC cannot be used (for example, self-hosted runners), create a short-lived granular access token using:

npm token create

Granular tokens can opt into Bypass 2FA for CI publishing and must expire within 90 days.

What You’ll See in Old Tutorials

Avoid these outdated patterns:

  • "main": "index.js" without exports—use exports for proper ESM resolution
  • Targeting Node 10/12/14—Node 18+ is supported, but new packages should target modern LTS versions (Node 20 or 22)
  • CommonJS-only packages—ESM is the standard; ship ESM-first
  • Permanent NPM_TOKEN in every workflow—classic tokens are fully revoked; use tokenless Trusted Publishing

Verify Before Publishing

Always test your package locally before publishing:

npm pack --dry-run

This shows exactly what files will be included. If you see src/ or node_modules/, your files field needs adjustment.

Conclusion

To create and publish an npm package in 2025: use ESM with proper exports, compile TypeScript to JavaScript with declarations, enable 2FA, and automate releases with npm Trusted Publishing. Skip the legacy patterns—they’ll cause headaches for you and your users.

FAQs

Publish compiled JavaScript with declaration files. Ship the dist folder containing your compiled JS and generated .d.ts files. This ensures compatibility across different TypeScript versions and build tools. Raw TypeScript forces consumers to compile your code, which can cause version conflicts and slower builds.

Use dependencies for packages your code bundles and ships. Use peerDependencies for packages consumers must provide, like React for a React hook. This prevents duplicate copies of large libraries in the final bundle. Your package expects the consumer's version rather than bundling its own.

Scoped packages like @username/package-name default to private on npm. Free npm accounts cannot publish private packages, so you must explicitly set --access public. This flag tells npm to publish the package publicly. You only need it for the first publish; subsequent versions inherit the access level.

Use npm deprecate @scope/package to warn users without removing the package. Full unpublishing with npm unpublish is restricted to packages published within 72 hours or those with minimal downloads. For abandoned packages, deprecation is preferred as it warns users while preserving existing installations.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay