Chapter 20: Node.js Publish a Package
How to publish a package to npm in Node.js — written as if I’m sitting next to you, doing every step together on your laptop, explaining why we do each thing, what can go wrong, and what experienced developers actually do in 2025–2026.
Let’s publish a small but realistic utility package from zero to npm — step by step.
Goal of this tutorial
We’re going to create, test, prepare, and publish a small utility package called @webliance/date-helpers that contains a few useful date formatting functions.
You will learn:
- How to structure a publishable package
- The most important files (package.json, README, LICENSE, .npmignore…)
- How to use scoped packages (@username/…)
- How to publish safely (public vs private)
- How to version and update packages
- Common gotchas and best practices
Step 1 – Decide: scoped or unscoped?
| Type | Name example | Who can publish updates? | Most common in 2025–2026 |
|---|---|---|---|
| Unscoped | date-helpers | Anyone who gets the name first | Rare now (very competitive) |
| Scoped | @webliance/date-helpers | Only you (or your organization) | Recommended — almost everyone uses scoped packages |
Decision for this tutorial: We’ll publish a scoped package @webliance/date-helpers
Step 2 – Create the project folder & initialize
|
0 1 2 3 4 5 6 7 8 9 10 |
mkdir date-helpers cd date-helpers # Important: use the SAME name as you plan to publish npm init -y |
Now edit package.json — this is the most important file.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
{ "name": "@webliance/date-helpers", "version": "1.0.0", "description": "Small collection of modern date formatting helpers", "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", "type": "module", "files": [ "dist" ], "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.js" } }, "scripts": { "build": "tsup src/index.ts --format cjs,esm --dts --clean", "test": "vitest run", "prepublishOnly": "npm run build", "lint": "eslint . --ext .ts", "format": "prettier --write ." }, "keywords": [ "date", "formatting", "utils", "helpers" ], "author": "Webliance", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/webliance/date-helpers.git" }, "bugs": { "url": "https://github.com/webliance/date-helpers/issues" }, "homepage": "https://github.com/webliance/date-helpers#readme", "publishConfig": { "access": "public" } } |
Important fields explained:
| Field | Why it matters | Tip / Best practice |
|---|---|---|
| name | Must be unique on npm — scoped = @yourname/… | Use your npm username or org name |
| version | Semantic versioning — starts at 1.0.0 | Never publish 0.x.x as public packages |
| main / module / types | Tells Node.js / bundlers / TypeScript where to find code | Support both ESM + CommonJS + types |
| files | Which folders/files to include in published package | Usually only dist/ or lib/ |
| exports | Modern way to define entry points (replaces main in newer projects) | Very important for dual ESM/CommonJS support |
| prepublishOnly | Runs automatically before npm publish | Perfect place to run build |
| publishConfig.access | public or restricted | Must be public for open-source |
| license | Required — MIT is most common | Choose MIT / ISC / Apache-2.0 for open-source |
Step 3 – Create the actual code
src/index.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
export function formatDate(date: Date | string | number, options?: Intl.DateTimeFormatOptions): string { const d = new Date(date); return d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric', ...options }); } export function getRelativeTime(date: Date | string | number): string { const now = new Date(); const diffMs = now.getTime() - new Date(date).getTime(); const diffSec = Math.floor(diffMs / 1000); if (diffSec < 60) return `{diffSec} seconds ago`; if (diffSec < 3600) return `${Math.floor(diffSec / 60)} minutes ago`; if (diffSec < 86400) return `${Math.floor(diffSec / 3600)} hours ago`; return `${Math.floor(diffSec / 86400)} days ago`; } export const DATE_FORMATS = { short: { day: '2-digit', month: 'short', year: 'numeric' }, long: { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' } } as const; |
Step 4 – Add build step (very important)
We use tsup — one of the most popular tools in 2025–2026 for publishing libraries.
|
0 1 2 3 4 5 6 |
npm install -D tsup typescript @types/node |
Create tsconfig.json
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "declaration": true, "outDir": "./dist", "esModuleInterop": true, "strict": true, "skipLibCheck": true, "noEmitOnError": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } |
Now you can build:
|
0 1 2 3 4 5 6 |
npm run build |
This creates a clean dist/ folder with:
- index.js (CommonJS)
- index.mjs (ESM)
- index.d.ts (TypeScript types)
Step 5 – Create essential files
README.md (very important — people read this first!)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
# @webliance/date-helpers Small collection of modern, lightweight date formatting helpers. ## Installation ```bash npm install @webliance/date-helpers |
Usage
|
0 1 2 3 4 5 6 7 8 9 |
import { formatDate, getRelativeTime } from '@webliance/date-helpers'; console.log(formatDate(new Date())); // → 10 Feb 2026 console.log(getRelativeTime(new Date())); // → 2 minutes ago |
License
MIT
|
0 1 2 3 4 5 6 7 8 |
**LICENSE** file (copy MIT license text) **gitignore** |
node_modules dist .env
|
0 1 2 3 4 5 6 |
**.npmignore** (very important — controls what gets published) |
src/ tests/ *.test.ts tsconfig.json
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
--- ### Step 6 – Create an npm account & login 1. Go to https://www.npmjs.com/ 2. Sign up (or log in) 3. Verify your email In terminal: ```bash npm login |
→ enter username, password, email
Check you’re logged in:
|
0 1 2 3 4 5 6 |
npm whoami |
Should show your username
Step 7 – First publish!
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Make sure everything is built npm run build # Dry run — very useful before real publish npm publish --dry-run # If everything looks good: npm publish --access public |
You should see:
|
0 1 2 3 4 5 6 |
@webliance/date-helpers@1.0.0 |
Now go to https://www.npmjs.com/package/@webliance/date-helpers
It’s live!
Step 8 – Updating the package (next versions)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# 1. Make changes # 2. Update version using semver npm version patch # 1.0.0 → 1.0.1 npm version minor # 1.0.1 → 1.1.0 npm version major # 1.1.0 → 2.0.0 # or manually edit package.json version # 3. Build & publish again npm run build npm publish |
Summary – Checklist before publishing
- package.json has correct name, version, main, module, types, exports
- “private”: false or missing (not true)
- “publishConfig”: { “access”: “public” } (for scoped packages)
- files or .npmignore — only publish what’s needed
- Good README.md with install & usage example
- LICENSE file present
- build script works
- prepublishOnly runs build
- You are logged in (npm whoami)
- You ran npm publish –dry-run first
Bonus – Modern best practices 2026
- Use tsup, unbuild, rollup, or vite for building
- Add changeset or semantic-release for automated versioning
- Use vitest / uvu for tests — run before publish
- Publish beta versions with –tag beta
- Use GitHub Actions to automate publish on tag / release
Would you like to go deeper into any part?
- Setting up automated versioning & publishing with changesets
- Publishing beta / canary versions
- Making your package dual ESM + CommonJS perfectly
- Adding tests that run before publish
- Handling scoped organizations on npm
- What to do when you need to deprecate or unpublish versions
Just tell me which direction you want — I’ll continue with concrete examples. 😊
