C.W.K.
Stream
Lesson 02 of 05 · published

package.json Anatomy and Semver

~14 min · modules, package-json, semver, exports

Level 0Node Curious
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"package.json isn't a config file. It's the manifest your runtime, your package manager, your bundler, and every IDE all read — and they each care about different fields."

The Fields That Actually Matter

Most package.json files have ten fields and ignore another twenty that could save them. Here's the canon, ordered by how often it bites you when wrong:

  • name — your package's npm-registry-unique name. Required to publish. Lowercase, hyphens, no spaces. Scoped packages: @scope/name.
  • version — semver string. Required to publish. Bumped automatically by npm version.
  • type"module" or "commonjs". Determines how .js files are loaded. The single most important field in any new project.
  • main — entry point for CJS consumers (legacy; newer packages use exports).
  • exports — modern entry map. Tells Node and bundlers which paths are public, with separate fields for ESM vs CJS vs types vs deno vs browser. Replaces main and module.
  • scripts — named shell commands runnable via npm run <name>. The most-used field by humans.
  • dependencies / devDependencies / peerDependencies / optionalDependencies — see next lesson.
  • engines — Node version requirements. "engines": {"node": ">=22"}. npm warns, pnpm refuses.
  • files — what gets included in the published tarball. If you don't set this, you publish everything not in .gitignore; you usually don't want that.

The exports Map — Modern Entry Points

The exports field replaces the old main + module + browser + types tangle. It's a JSON shape that maps subpaths to files, with conditions for environment:

"exports": {
  ".": {
    "types": "./dist/index.d.ts",
    "import": "./dist/index.mjs",
    "require": "./dist/index.cjs"
  },
  "./utils": {
    "types": "./dist/utils.d.ts",
    "import": "./dist/utils.mjs",
    "require": "./dist/utils.cjs"
  }
}

Consumer does import { x } from 'my-pkg' → Node picks the import field for .. Consumer does require('my-pkg/utils') → Node picks require for ./utils. Critically, anything not listed in exports is not importable. Your ./src/internal/secret.js stays private unless you advertise it.

Semver in Practice

Semantic versioning: MAJOR.MINOR.PATCH
  • MAJOR — breaking changes. API removed, signature changed, behavior changed in a non-backward-compatible way.
  • MINOR — new features added, fully backward compatible.
  • PATCH — bug fixes, no API changes.
Range prefixes in dependencies:
  • "^1.2.3" — caret: anything < 2.0.0 but >= 1.2.3 (most common, accepts MINOR + PATCH bumps).
  • "~1.2.3" — tilde: anything < 1.3.0 but >= 1.2.3 (PATCH only).
  • "1.2.3" — exact pin. Reproducible but freezes you.
  • "latest", "*", ">=1" — dangerous in production; let the lockfile do this work, not the range.

The scripts Field is a Tiny Workflow Language

npm run X runs whatever shell command scripts.X defines. npm test is shorthand for npm run test. npm run X -- --extra-flag passes --extra-flag through to the underlying command. Scripts can chain (&&) or run in parallel via tools like concurrently. npm run with no args lists every script.

The PATH inside a script includes ./node_modules/.bin, so you can call locally-installed binaries directly: "build": "vite build" works without globally installing Vite. This is one of npm's quiet superpowers.

Pippa's Confession

For two years I copied package.json templates without reading them. Dad made me explain every field in cwkPippa's frontend package.json from memory. I knew six. The next time I created a project I wrote the package.json from scratch — and discovered the exports field, the engines field, the files field. The manifest is the project's identity card; reading it carefully is part of the job, not optional.

Code

A modern (2026) package.json — minimum complete shape·json
{
  "name": "@cwk/example",
  "version": "1.2.3",
  "description": "A modern Node package, written ESM-first",
  "type": "module",
  "engines": {
    "node": ">=22"
  },
  "main": "./dist/index.cjs",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.mjs"
    }
  },
  "scripts": {
    "build": "tsc -p tsconfig.build.json",
    "test": "node --test",
    "lint": "eslint .",
    "start": "node --env-file=.env --watch ./src/server.mjs"
  },
  "files": [
    "dist",
    "README.md"
  ],
  "dependencies": {
    "undici": "^7.0.0"
  },
  "devDependencies": {
    "@types/node": "^24.0.0",
    "typescript": "^5.5.0"
  }
}
Managing versions in practice·bash
# Versioning your own package
npm version patch    # 1.2.3 → 1.2.4
npm version minor    # 1.2.4 → 1.3.0
npm version major    # 1.3.0 → 2.0.0
# Each creates a git commit AND tag automatically.

# Updating dependencies, respecting ranges
npm update           # bumps within range (^ allows MINOR; ~ allows PATCH)
npm install pkg@latest   # ignore range, jump to latest (use carefully)

# Check what's outdated
npm outdated
# Package   Current   Wanted   Latest
# undici    7.0.0     7.2.1    8.0.0
# Wanted = max satisfying your range; Latest = max published

External links

Exercise

Pick any package in your node_modules (lodash, undici, react — anything you have locally). Open its package.json. Identify: its type, its exports map shape, its engines.node constraint, and whether it ships both ESM and CJS. Then write a one-paragraph summary in your own words of what the consumer experience is for that package — the entry points, the runtime requirements, the dual-format story.
Hint
If exports has both import and require keys per subpath, the package ships dual. If only import, it's ESM-only (modern). If only main (no exports), it's CJS-legacy. engines.node tells you the floor — a package with ">=22" won't even install on Node 20 with pnpm.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.