How would you build and publish your own npm package
Scaffold with package.json, build to ESM (+ CJS) with TypeScript declarations, set exports/main/module/types fields and the files allowlist, version with semver, test and lint, then npm publish (with CI, provenance, and a changelog). Keep the API small and well-documented.
Publishing an npm package is straightforward; publishing a good one is about correct packaging, clear API, and release hygiene.
1. Scaffold
npm init→package.json. Pick a clear, available name (or a scope:@yourorg/pkg).- Decide scope: public vs private (
"private": trueorpublishConfig.access). - Choose a license, write a real README (install, usage, API, examples).
2. Build & module formats
Ship compiled, consumable output — not raw TS/JSX:
- TypeScript source → emit ESM (and CJS if you need to support older consumers) plus
.d.tstype declarations. - Use a bundler/build tool —
tsup,unbuild, Rollup, or Vite library mode — they handle ESM+CJS+types cleanly. - Configure
package.jsonfields correctly — this is where most packages get it wrong:
``json { "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" } }, "files": ["dist"], "sideEffects": false } ``
exportsis the modern source of truth for entry points.filesallowlist (or.npmignore) so you don't publish tests/src/config.sideEffects: falsehelps consumers tree-shake.peerDependenciesfor things likereact— don't bundle the consumer's framework.
3. Quality gates
- Tests (Vitest/Jest), lint, type-check.
npm pack --dry-run(orpublint/arethetypeswrong) to inspect exactly what ships and verify the package resolves correctly.- Keep the public API small and intentional — every export is a forever contract.
4. Version & publish
- Semver — patch (fix), minor (feature, backward-compatible), major (breaking).
npm login, thennpm publish(--access publicfor a scoped public package).- A
prepublishOnlyscript to build + test so you never publish stale artifacts. - Use
npm versionto bump + tag; maintain a CHANGELOG.
5. Automate (for anything real)
- CI publish — GitHub Actions on a release tag;
changesetsorsemantic-releasefor automated versioning + changelog. - npm provenance (
--provenancein CI) for supply-chain trust. - Test it for real:
npm packand install the tarball in a sample project, ornpm link.
6. Maintain
- Respond to issues, keep deps updated, document breaking changes, deprecate gracefully (
npm deprecate).
The framing
"Scaffold package.json, build TS to ESM+CJS with .d.ts types, and — the part people get wrong — set exports/types/files correctly so it resolves in every consumer. Gate with tests/lint and publint, version with semver, and automate publishing via CI with changesets and provenance. Keep the API minimal — every export is a contract you'll maintain."
Follow-up questions
- •Why does the package.json 'exports' field matter and what does it replace?
- •When do you use peerDependencies vs dependencies?
- •How do you support both ESM and CJS consumers?
- •How would you automate versioning and changelog generation?
Common mistakes
- •Publishing raw source instead of compiled output, or shipping no type declarations.
- •Misconfigured main/module/exports so it doesn't resolve for some consumers.
- •Publishing tests/src/config because there's no files allowlist.
- •Bundling react (or another framework) instead of declaring it a peerDependency.
- •A huge, accidental public API surface.
Performance considerations
- •Ship ESM and mark sideEffects:false so consumers tree-shake. Keep the dependency footprint small — every dep becomes the consumer's bundle weight. Provide granular entry points if the package is large.
Edge cases
- •Dual ESM/CJS support and the 'dual package hazard'.
- •Scoped packages needing --access public.
- •Breaking changes requiring a major bump and migration notes.
- •Tree-shaking broken by missing sideEffects:false.
Real-world examples
- •A shared internal component library or hooks package published to a private registry.
- •tsup-built dual ESM/CJS package, released via changesets in GitHub Actions with provenance.