package.json
package.json is your project’s manifest: name, version, scripts, dependencies, entry points, engine requirements. Knowing the fields well prevents wasted hours debugging exports, peer dependencies, and lockfile drift.
Scripts, deps, exports, engines, types
EXAMPLE
{
"name": "my-app",
"version": "1.2.3",
"description": "My amazing app",
"type": "module",
"private": true,
"engines": { "node": ">=20" },
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" },
"./utils": { "types": "./dist/utils.d.ts", "import": "./dist/utils.js" },
"./package.json": "./package.json"
},
"files": ["dist", "README.md", "LICENSE"],
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"start": "node dist/server.js",
"test": "vitest",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write \".\"",
"prepare": "husky install"
},
"dependencies": { "express": "^4.21.0", "zod": "^3.23.0" },
"devDependencies": { "vitest": "^2.0.0", "typescript": "^5.5.0" },
"peerDependencies": { "react": ">=18.0.0" },
"optionalDependencies": { "fsevents": "^2.3.0" },
"keywords": ["web", "api"],
"author": "Mara <mara@example.com>",
"license": "MIT",
"repository": { "type": "git", "url": "https://github.com/me/my-app.git" },
"bugs": { "url": "https://github.com/me/my-app/issues" },
"homepage": "https://github.com/me/my-app#readme"
}
// 1) name + version — semver matters
// • Scoped packages start with @ (e.g. '@my-org/utils')
// • semver: MAJOR.MINOR.PATCH; pre-release tags: 1.2.3-alpha.1
// 2) type field
// • 'module' — .js files are ESM
// • 'commonjs' (or omitted) — .js files are CJS
// • .mjs always ESM; .cjs always CJS regardless of type
// 3) main / module / browser / types
// • main — legacy entry; what require() picks up
// • module — deprecated bundler hint; replaced by exports
// • browser — bundler picks for browser builds
// • types — TypeScript types entry (or per-condition types in exports)
// 4) exports — modern entrypoint declaration (preferred)
// • Restricts what consumers can import (subpath encapsulation)
// • Conditions: import, require, node, browser, types, default
// • Anything NOT listed is unreachable — deliberate API surface
// 5) files — what's published to the registry
// • Defaults include README, LICENSE, package.json + main + everything tracked by npm
// • Use 'files' allowlist; or .npmignore (rare); 'files' is recommended
// 6) scripts
// • npm run <name> runs the script in the package directory
// • npm test / npm start / npm restart / npm stop are special (no 'run')
// • Lifecycle hooks: prepare, prepublish, preversion, postinstall (avoid for security)
// • Cross-platform: use cross-env, rimraf for portable scripts
// 7) dependencies tiers
// • dependencies — runtime requirements (ship in install)
// • devDependencies — build / test / tooling (NOT installed for consumers)
// • peerDependencies — your package expects the host to provide them (libraries)
// • optionalDependencies — install if possible; failures are ignored
// • bundledDependencies — copy into the published tarball (rare)
// 8) workspaces (monorepo)
{
"workspaces": ["packages/*", "apps/*"],
"scripts": { "build": "npm run build --workspaces --if-present" }
}
// Sub-packages share node_modules; one install across the whole repo.
// 9) engines + engineStrict (deprecated)
{
"engines": { "node": ">=20", "npm": ">=10" }
}
// npm warns when the host doesn't satisfy engines; pnpm and Yarn refuse.
// Use 'volta' or '.nvmrc' to PIN node version per project for the team.
// 10) sideEffects — tree-shaking hint
{
"sideEffects": false
}
// or list specific files: ["./src/polyfill.js", "*.css"]
// Bundlers drop unused exports more aggressively.
// 11) bin — CLI scripts
{
"bin": { "my-cli": "./bin/my-cli.js" }
}
// After install, my-cli is on PATH. Make the file executable: chmod +x bin/my-cli.js
// Add a shebang: #!/usr/bin/env node
// 12) Build the manifest in CI — npm view / npm publish --dry-run
npm publish --dry-run // see what would be published
npm pack --dry-run // same; lists contents
npm publish --provenance // sigstore-signed (recommended)
// 13) Migration tips
// • Switching to 'type: module' may break some CJS-only dependencies
// • Adding 'exports' is BREAKING — consumers using internal paths get errors
// • Renaming 'main' to point at a new location — bump major version
// • Removing a published version is destructive — use 'deprecate' instead
// 14) Common bugs
// • Setting type: module without converting require → ERR_REQUIRE_ESM
// • exports map missing './package.json' — tooling that reads it breaks
// • files allowlist misses dist/ — npm publish ships nothing
// • peerDependencies in devDependencies → consumers don't get the hint
// • Pinning every dep at exact (no ^/~) → security patches require manual bumps
// • Forgetting to commit package-lock.json — every install resolves a fresh tree
// • 'prepare' script running heavy build at install time — slow for downstream users
Why it matters
Treat package.json as your project’s public contract. Use exports to lock down your API surface, allowlist published files, pin Node via engines + .nvmrc, and split deps correctly — runtime in dependencies, tooling in devDependencies, host requirements in peerDependencies.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
{
"name": "my-app",
"type": "module",
"scripts": { "start": "node src/index.js" }
}
Try it Yourself »
Discussion
Loading…