Tips & Tricks (Updated: 6/2/2026)

Create and Publish an npm Package with Claude Code

Use Claude Code to build an npm package with tsup, exports, types, npm pack checks, README, and CI publishing.

Create and Publish an npm Package with Claude Code

Asking Claude Code to “make an npm package” is easy. Publishing a package that other developers can install safely is a different job. You need a clear package.json, correct exports, generated type declarations, a README that matches the API, npm pack checks, and a release workflow that does not depend on a long-lived token sitting in a random secret.

This guide uses a small TypeScript string utility package as the running example. The goal is not to let Claude Code publish blindly. The goal is to use Claude Code as a reviewer and pair programmer while you keep the release contract explicit. For the official baseline, keep the npm docs for package.json, scoped public packages, npm pack, trusted publishing, and the Claude Code overview nearby.

Start with a Package Brief

The first prompt should not be a vague request for boilerplate. Give Claude Code the package name, audience, runtime, module formats, verification commands, and publishing policy. Those choices affect each other. If type: "module" is present, CJS output needs a .cjs file. If exports is present, consumers can only import the paths you expose. If files is too broad, test fixtures or internal notes can be shipped by accident.

AreaDecision for this articleWhat Claude Code should check
Name@acme/string-kitwhether scoped public publishing needs --access public
UsersNode.js and TypeScript userswhether both ESM import and CJS require work
Buildtsup emits ESM, CJS, and declarationswhether dist contains the files referenced by exports
Package contentsonly dist, README.md, and LICENSEwhether npm pack --dry-run exposes surprises
ReleaseGitHub Actions plus npm Trusted Publishingwhether the workflow avoids unnecessary long-lived tokens

The workflow is easier to review when you draw the lifecycle before editing files:

flowchart LR
  A["Package brief"] --> B["package.json"]
  B --> C["src/index.ts"]
  C --> D["Vitest"]
  D --> E["tsup build"]
  E --> F["npm pack dry-run"]
  F --> G["CI publish"]

If your package is a CLI, read Claude Code CLI tool development after this article. For the prompting habits behind this workflow, pair it with Claude Code productivity tips.

Create the Minimal Project

Start in a clean directory before moving the idea into a larger repository. A small package makes it obvious whether a failure comes from the package settings or from unrelated workspace tooling. When asking Claude Code to generate the project, include these commands as the expected setup path.

mkdir string-kit
cd string-kit
npm init -y
npm install -D typescript tsup vitest @types/node
mkdir src scripts

The package.json is the public contract. main supports CJS consumers, module helps older bundlers, types points TypeScript to declarations, and exports controls modern entry points. The files array is deliberately small so unpublished drafts, tests, and local configuration do not leak into the tarball.

{
  "name": "@acme/string-kit",
  "version": "0.1.0",
  "description": "Small TypeScript string utilities used as an npm package example.",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./package.json": "./package.json"
  },
  "files": ["dist", "README.md", "LICENSE"],
  "sideEffects": false,
  "scripts": {
    "build": "tsup",
    "test": "vitest run",
    "docs": "node scripts/write-readme.mjs",
    "test:pack": "npm pack --dry-run",
    "prepublishOnly": "npm run test && npm run build && npm run test:pack"
  },
  "keywords": ["string", "typescript", "utilities"],
  "license": "MIT",
  "devDependencies": {
    "@types/node": "^22.15.0",
    "tsup": "^8.5.0",
    "typescript": "^5.8.0",
    "vitest": "^3.2.0"
  }
}

The TypeScript config lets tsup handle emitted JavaScript while TypeScript checks the source. moduleResolution: "Bundler" is a practical default for modern library code because it aligns better with packages that use exports.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "skipLibCheck": true,
    "noEmit": true
  },
  "include": ["src", "tsup.config.ts"]
}

Add Real Implementation and Tests

A public package article should not stop at pseudocode. The example below exports four small functions: slugify for ASCII slugs, truncate for bounded display strings, interpolate for simple message templates, and byteLength for UTF-8 byte counts. The code is intentionally small enough to audit but real enough to build, test, and package.

export function slugify(input: string): string {
  return input
    .normalize("NFKD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");
}

export function truncate(input: string, maxLength: number, suffix = "..."): string {
  if (!Number.isInteger(maxLength) || maxLength < 1) {
    throw new RangeError("maxLength must be a positive integer");
  }
  if (suffix.length >= maxLength) {
    throw new RangeError("suffix must be shorter than maxLength");
  }
  if (input.length <= maxLength) return input;
  return `${input.slice(0, maxLength - suffix.length)}${suffix}`;
}

export function interpolate(template: string, values: Record<string, string | number>): string {
  return template.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (match, key: string) => {
    return Object.hasOwn(values, key) ? String(values[key]) : match;
  });
}

export function byteLength(input: string): number {
  return new TextEncoder().encode(input).length;
}

The tests cover normal behavior, boundaries, exceptions, and Unicode. This is the level of specificity you should request from Claude Code. “Add tests” is too broad. “Test accents, unknown placeholders, invalid lengths, and UTF-8 byte counts” gives the agent something concrete to verify.

import { describe, expect, it } from "vitest";
import { byteLength, interpolate, slugify, truncate } from "./index";

describe("slugify", () => {
  it("turns a title into an npm-friendly slug", () => {
    expect(slugify("Hello npm Package!")).toBe("hello-npm-package");
  });

  it("removes accents before replacing separators", () => {
    expect(slugify("Crème brûlée utils")).toBe("creme-brulee-utils");
  });
});

describe("truncate", () => {
  it("keeps short text unchanged", () => {
    expect(truncate("short", 10)).toBe("short");
  });

  it("adds a suffix inside the requested length", () => {
    expect(truncate("Claude Code package", 12)).toBe("Claude Co...");
  });

  it("rejects invalid lengths", () => {
    expect(() => truncate("abc", 2)).toThrow(RangeError);
  });
});

describe("interpolate", () => {
  it("replaces known placeholders and keeps unknown ones", () => {
    expect(interpolate("Hi {{ name }}, ship {{pkg}} {{missing}}", {
      name: "Masa",
      pkg: "@acme/string-kit",
    })).toBe("Hi Masa, ship @acme/string-kit {{missing}}");
  });
});

describe("byteLength", () => {
  it("counts UTF-8 bytes", () => {
    expect(byteLength("npm")).toBe(3);
    expect(byteLength("日本語")).toBe(9);
  });
});

Configure tsup and Generate README

tsup keeps this package setup lightweight. The important detail is outExtension: ESM is emitted as .js, while CJS is emitted as .cjs. That matches the package.json entry points and prevents require users from loading an ESM file by mistake. sourcemap: false is intentional for this tutorial because public packages should not ship internal source maps unless you have decided that tradeoff clearly.

import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm", "cjs"],
  dts: true,
  clean: true,
  sourcemap: false,
  minify: false,
  target: "es2022",
  outDir: "dist",
  outExtension({ format }) {
    return { js: format === "esm" ? ".js" : ".cjs" };
  },
});

README drift is a common source of package quality problems. A tiny generator is enough to keep installation and usage examples aligned with the exported API. Claude Code can update this file when the API changes, and CI can run npm run docs before packing.

import { writeFile } from "node:fs/promises";

const fence = String.fromCharCode(96).repeat(3);
const readme = `# @acme/string-kit

Small TypeScript string utilities packaged with tsup.

## Install

${fence}bash
npm install @acme/string-kit
${fence}

## Usage

${fence}ts
import { slugify, truncate } from "@acme/string-kit";

console.log(slugify("Hello npm Package!"));
console.log(truncate("Claude Code package", 12));
${fence}
`;

await writeFile(new URL("../README.md", import.meta.url), readme);

Verify the Tarball Before Publishing

Do not treat npm publish as the first real package check. npm pack --dry-run shows the files that would be sent to the registry. Review that output before every first release and whenever the build layout changes. The most important question is simple: would a clean project be able to use the package from the tarball alone?

npm run docs
npm test
npm run build
npm pack --dry-run
node -e "import('./dist/index.js').then((m)=>console.log(m.slugify('Hello ESM')))"
node -e "const m=require('./dist/index.cjs'); console.log(m.slugify('Hello CJS'))"

For a stronger smoke test, install the generated .tgz into a separate directory. This catches packages that accidentally work only because local source files are still present beside the build output.

npm pack
mkdir ../string-kit-smoke
cd ../string-kit-smoke
npm init -y
npm install ../string-kit/acme-string-kit-0.1.0.tgz
node -e "import('@acme/string-kit').then((m)=>console.log(m.truncate('Claude Code package', 12)))"

There are at least three practical use cases for this pattern. First, a product team can share string formatting rules across web, admin, and documentation apps without copying helpers everywhere. Second, content workflows can use interpolate and truncate to keep descriptions, cards, and release notes consistent. Third, a design system or CLI can publish small utilities separately so application teams can upgrade them with normal SemVer instead of waiting for a large monorepo release.

Publish from GitHub Actions

Publishing from CI gives reviewers a repeatable path. npm Trusted Publishing lets supported CI providers publish through OIDC, which removes the need for a long-lived npm token in many setups. Configure the trusted publisher in npm first, then allow publishing only from a release event. If your organization still uses tokens, review npm’s provenance and 2FA guidance instead of copying an old workflow.

name: package

on:
  push:
    branches: [main]
  pull_request:
  release:
    types: [published]

permissions:
  contents: read
  id-token: write

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: https://registry.npmjs.org
          cache: npm
      - run: npm ci
      - run: npm run docs
      - run: npm test
      - run: npm run build
      - run: npm pack --dry-run

  publish:
    if: github.event_name == 'release'
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: https://registry.npmjs.org
          cache: npm
      - run: npm ci
      - run: npm run docs
      - run: npm test
      - run: npm run build
      - run: npm publish --access public

If the package will live in a monorepo or needs controlled version notes, connect this with Claude Code and Changesets versioning. It gives Claude Code a better structure for reviewing patch, minor, and major changes.

Common Failure Modes

The first failure is using only main and forgetting exports. Some old examples still work that way, but modern consumers expect explicit entry points. Ask Claude Code to verify that the files referenced by exports, main, and types actually exist after npm run build.

The second failure is publishing too much. Without a tight files array, a package can include test fixtures, local notes, screenshots, source maps, or configuration files. Make npm pack --dry-run output part of the acceptance criteria, not an optional afterthought.

The third failure is README drift. A renamed export, changed package scope, or removed default export can leave the README wrong even though tests pass. Generating README content from a script is not mandatory, but some repeatable check should exist.

The fourth failure is overpromising Unicode support. The sample truncate uses JavaScript string length, not grapheme cluster segmentation. If your package promises emoji-safe display length, design a separate function with Intl.Segmenter, document browser and Node support, and test it explicitly.

The fifth failure is asking Claude Code to perform release authority decisions. Let Claude Code edit files, produce checklists, and explain npm pack output. Keep package naming, npm access settings, 2FA policy, trusted publisher setup, and the final release approval as human decisions.

Copy-Paste Claude Code Prompt

Use a prompt like this when you want a reviewable package instead of a random boilerplate dump:

Create a TypeScript npm package.

Goal:
- Package name: @acme/string-kit
- Support both ESM import and CJS require
- Use tsup to emit dist/index.js, dist/index.cjs, and dist/index.d.ts
- Include README generation, Vitest tests, and npm pack verification

Constraints:
- Only touch package.json, tsconfig.json, tsup.config.ts, src, scripts, and .github/workflows
- Do not use pseudocode; the project must run after npm install
- Do not publish source maps or unnecessary test files in the package tarball

Acceptance criteria:
- npm test passes
- npm run build passes
- npm pack --dry-run output is summarized
- ESM import and CJS require smoke tests are shown
- List the human release checks before npm publish

CTA: Keep the Release Pattern Close

npm package publishing is not a one-time setup. Versions, README examples, CI Node versions, dependencies, and registry permissions all change. Start with the free Claude Code cheatsheet if you want safer prompts and verification commands nearby. For reusable package, review, and content templates, see the ClaudeCodeLab products. For team rollout, CLAUDE.md, CI policy, and review training, use the Claude Code training and consultation page.

After trying the workflow in a temporary Windows directory, the sample passed npm install, npm test, npm run build, npm pack --dry-run, and both ESM/CJS node -e smoke tests. In Masa’s day-to-day workflow, asking Claude Code to explain the npm pack output before release catches the most practical mistakes: stale README examples, missing declaration files, and accidental tarball contents.

Summary

Claude Code can speed up npm package creation, but the release contract still needs to be explicit. Start with package boundaries, then wire package.json, exports, types, tsup, tests, README generation, npm pack, and CI into one workflow.

Before release, ask three questions: did you inspect dist, did you inspect the tarball, and did you import the package through the same entry points users will use? Put those checks into the Claude Code prompt and the package becomes much easier to trust.

#Claude Code #npm #繝代ャ繧ア繝シ繧ク蜈ャ髢・ #TypeScript #OSS
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.