package.json Explained
package.json is the manifest at the heart of every Node project — a single JSON file that declares what your project is (name, version, entry point), what it needs (dependencies), and what it does (scripts). npm, yarn, pnpm, bundlers, and Node itself all read it. Understanding each field turns it from boilerplate into a precise control surface.
A realistic example
package.json
{
"name": "@acme/widget-api",
"version": "2.4.1",
"description": "REST API for managing widgets",
"type": "module",
"main": "./dist/index.js",
"exports": {
".": "./dist/index.js",
"./client": "./dist/client.js"
},
"bin": { "widget": "./bin/cli.js" },
"engines": { "node": ">=20" },
"scripts": {
"dev": "node --watch src/index.js",
"test": "node --test",
"build": "tsc -p ."
},
"dependencies": { "express": "^4.19.2" },
"devDependencies": { "typescript": "^5.4.0" },
"license": "MIT"
}The identity fields
Field | Purpose | Rules |
|---|---|---|
| Package identifier | Lowercase, URL-safe, ≤214 chars; may be scoped ( |
| Current version | Must be valid semver |
| One-line summary | Shown in |
| Legal terms | An SPDX id like |
| Block publishing | Set |
Entry points: main, exports, type
These three fields decide what code loads when someone imports your package and how it is interpreted:
Field | Controls |
|---|---|
| The legacy entry file |
| Modern entry map — defines public subpaths and hides everything else |
|
|
| Maps command names to scripts, installed onto |
The dependency fields
Field | For | Installed when |
|---|---|---|
| Code needed at runtime | Always (incl. by consumers of your package) |
| Tooling: tests, build, lint | Only on |
| A host package you plug into | Not auto-installed; consumer must provide it |
| Nice-to-have; tolerate failure | Attempted, but install continues if it fails |
The crucial dependencies vs devDependencies distinction has its own page: dependencies vs devDependencies. Version ranges like ^4.19.2 are explained in Semantic Versioning.
Scripts
The scripts map defines named commands you run with npm run <name>. A few names are special — start, test, stop, restart — and run without the run keyword (npm test):
npm run dev # runs the "dev" script npm test # shortcut for the "test" script npm start # shortcut for the "start" script
Scripts get node_modules/.bin prepended to PATH, so locally-installed tools run by bare name. Full mechanics — lifecycle hooks, pre/post, passing args with -- — are in npm Scripts.
Constraining the environment
{
"engines": { "node": ">=20.0.0", "npm": ">=10" },
"os": ["!win32"],
"cpu": ["x64", "arm64"]
}Controlling what gets published
The files array is an allow-list of what ends up in the published tarball — usually just your build output, never source or tests:
{
"files": ["dist", "README.md"],
"main": "./dist/index.js"
}Editing it: by hand vs by CLI
Run
npm install <pkg>/npm uninstall <pkg>— npm editsdependenciesand the lockfile for you. Prefer this over hand-editing versions.Run
npm pkg set scripts.lint="eslint ."to script edits without opening the file.Hand-edit freely for
scripts,exports,engines, metadata — just keep it valid JSON (no comments, no trailing commas).Run
npm pkg get dependenciesto read fields programmatically.