"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 bynpm 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 useexports).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. Replacesmainandmodule.scripts— named shell commands runnable vianpm 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
- 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.
"^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
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.