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.
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.
| Area | Decision for this article | What Claude Code should check |
|---|---|---|
| Name | @acme/string-kit | whether scoped public publishing needs --access public |
| Users | Node.js and TypeScript users | whether both ESM import and CJS require work |
| Build | tsup emits ESM, CJS, and declarations | whether dist contains the files referenced by exports |
| Package contents | only dist, README.md, and LICENSE | whether npm pack --dry-run exposes surprises |
| Release | GitHub Actions plus npm Trusted Publishing | whether 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.
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.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Permission Safety Ladder: Expand Access Without Losing Control
A beginner-friendly ladder for moving Claude Code from read-only to limited edits, proof commands, and deploy checks.
Claude Code Small PR Proof Pack: Make Tiny Changes Reviewable
A practical proof pack for Claude Code PRs: diff, checks, public URL, CTA path, and rollback note.
Claude Code Review Gate Before Commit: Diff, Tests, Public URL, and CTA Checks
A commit-time review gate for Claude Code work: diff scope, build, public URL, revenue CTA links, missing tests, and unrelated files.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.