package.json Fields Explained: Every Key in package.json
Last updated:
package.json is a JSON file that describes a Node.js or JavaScript package. Only two fields are strictly required — name and version — but modern packages typically set 10–20 fields across six groups: identity, dependencies, entry points, scripts, engines, and publishing. This reference covers every field grouped by what it does, not alphabetically.
Need to validate a package.json file? Paste it into Jsonic's JSON Validator — it pinpoints syntax errors with line and column numbers.
Identity fields: name, version, description
The identity group answers who and what this package is. Two fields here are load-bearing for the whole npm ecosystem: name uniquely identifies the package across the registry, and version drives every install resolution.
{
"name": "my-package",
"version": "1.4.2",
"description": "Short description shown in npm search results",
"keywords": ["json", "parser", "schema"],
"license": "MIT",
"author": "Jane Doe <jane@example.com> (https://example.com)",
"homepage": "https://github.com/me/my-package#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/me/my-package.git"
},
"bugs": { "url": "https://github.com/me/my-package/issues" }
}name rules: lowercase, 1–214 characters, no leading dot or underscore, no spaces, URL-safe. Scoped names like @your-org/pkgpublish to the owner's scope. The npm registry rejects names that conflict with built-in Node.js modules (e.g., http, fs).
version must follow semver strictly — MAJOR.MINOR.PATCH, with optional pre-release (1.0.0-beta.1) and build metadata (1.0.0+sha.5114f85). Use npm version patch to bump automatically and create a matching git tag.
Dependencies: runtime, dev, peer, optional
package.json declares dependencies in four objects with distinct semantics. Each maps package name to a version range; the lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock) records the exact resolved versions.
| Field | Installed when | Bundled to consumers? | Typical use |
|---|---|---|---|
dependencies | Always (production install) | Yes — transitively | Runtime libraries (express, axios) |
devDependencies | Only on npm install in your repo | No | Build tools, test runners, types |
peerDependencies | Host project provides (auto-installed by npm 7+) | No | Plugins, React components, frameworks |
optionalDependencies | Best-effort; install failure is non-fatal | If installed | Native bindings (fsevents on macOS) |
{
"dependencies": {
"express": "^4.19.2",
"zod": "^3.23.8"
},
"devDependencies": {
"typescript": "^5.5.0",
"vitest": "^1.6.0",
"@types/node": "^20.14.0"
},
"peerDependencies": {
"react": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "^2.3.3"
}
}Version range prefixes: ^1.2.3 allows compatible 1.x.y (patches + minors),~1.2.3 allows only patches (1.2.y), 1.2.3 pins exactly,>=1.2.3 <2.0.0 is the explicit form of ^. Use exact pins (1.2.3) for tools where reproducibility matters more than getting bug fixes (e.g., your bundler).
peerDependenciesMeta(npm 7+) marks peers as optional so npm doesn't auto-install them: "peerDependenciesMeta": {"react": {"optional": true}}.
Entry points: main, module, exports, types
When another package require()s or imports yours, Node.js looks at the entry-point fields to decide which file to load. Modern dual-format packages typically declare four fields. The exports field (Node 12.7+) takes precedence over the rest and is the recommended modern approach.
| Field | Resolves for | When to set |
|---|---|---|
main | require() (CommonJS) | Always, for backward compat |
module | Bundler hint for ES module build | Legacy bundlers; superseded by exports |
types (or typings) | TypeScript type lookup | If you ship .d.ts files |
exports | Modern resolver (Node 12.7+, bundlers) | All new packages — controls what is importable |
bin | CLI executables installed to .bin/ | If your package provides a command |
{
"name": "my-lib",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
}
},
"bin": {
"my-lib": "./dist/cli.js"
}
}The exports map supports conditional exports: keys like import, require, browser, node,development, and production let one package ship different implementations per environment. Once exports is set, files NOT listed are unreachable to consumers — you get explicit encapsulation.
Scripts: npm run, lifecycle hooks, pre/post
scripts is the most-used field in day-to-day work. Each key becomes an invokable target via npm run <name>. A handful of reserved names run automatically: start (on npm start), test (on npm test), prepare (on npm install andnpm publish), and the pre*/post* hooks for every script.
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"test": "vitest",
"lint": "eslint .",
"format": "prettier --write .",
"typecheck": "tsc --noEmit",
"prebuild": "rimraf dist",
"postbuild": "echo 'build complete'",
"prepare": "husky install",
"release": "npm run typecheck && npm run test && npm publish"
}
}Inside scripts: ./node_modules/.bin is on PATH, so locally-installed CLIs (tsc, jest, eslint) are callable by name. Cross-platform compatibility: avoid && chains where one side is shell-specific; use npm-run-all or concurrently for parallel scripts; use cross-env for environment-variable assignments that work on Windows.
engines, files, publishConfig: publishing controls
These fields determine what gets published and where, plus what runtime environment your package supports.
{
"engines": {
"node": ">=20.0.0",
"npm": ">=10.0.0"
},
"files": ["dist", "README.md", "LICENSE"],
"private": false,
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"browserslist": ["> 0.5%", "last 2 versions", "not dead"]
}engines— semver ranges for runtimes. Vercel, Netlify, and Fly readengines.nodeto pick the build runtime. Pair withengine-strict=truein.npmrcto turn the warning into an install error.files— allowlist of paths included in the published tarball. Without it, npm publishes nearly everything (minus a hardcoded ignore list). Always set this for libraries:["dist"]usually suffices.private: true— npm refuses to publish. Use on internal apps, monorepo roots, and example projects to prevent accidentalnpm publish.publishConfig— overrides per-publish settings.access: publicis required when publishing a scoped package as public.browserslist— read by Babel, PostCSS Autoprefixer, ESLint, and others to target supported browsers. Standard query strings; check support at browserslist.dev.
Before publishing, always run npm pack --dry-run to see exactly which files will end up in the tarball. This catches accidentally-shipped source files, tests, and secrets in seconds.
workspaces: monorepos in package.json
workspaces turns a single repository into a multi-package monorepo managed by npm 7+, pnpm, or yarn. The root package.json lists the package directories (or globs); each subdirectory has its own package.json and can depend on its siblings by name. The package manager symlinks them under node_modules/.
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*", "apps/web", "apps/cli"]
}Workspace commands: npm install -w packages/ui zod installs zod into a specific workspace. npm run build --workspaces runs the build script in every workspace that defines one. pnpm and yarn use --filter instead of -w but the package.json field is the same.
The monorepo root must have "private": true — workspaces only work when the root is non-publishable.
Common package.json errors and fixes
The strict JSON grammar bites teams that come from looser config formats. The five errors below cover >90% of real-world package.json failures.
| Error | Cause | Fix |
|---|---|---|
Unexpected token } in JSON | Trailing comma after last item | Remove the trailing comma; use a JSON validator |
Invalid name: must not contain capital letters | Uppercase letter in name | Lowercase the name; npm enforces this |
Invalid version: ... | Non-semver version like 1.0 | Use 1.0.0 — three numeric segments required |
EPUBLISHCONFLICT on npm publish | Version already published | Bump version: npm version patch (or minor/major) |
EUSAGE Cannot read property 'replace' of undefined | Missing required field (often name) or malformed exports map | Re-run npm init or validate with npm pkg fix |
Key terms
- semver (Semantic Versioning)
- A version-numbering convention with three parts: MAJOR.MINOR.PATCH. Bump MAJOR for breaking changes, MINOR for new features, PATCH for fixes. Defined at semver.org.
- Lockfile
- A separate file (
package-lock.json,pnpm-lock.yaml,yarn.lock) that records the exact versions installed, including transitive dependencies. Commit it to version control for reproducible builds. - Conditional exports
- An
exportsmap where keys (import,require,browser, etc.) select different files per environment. Lets one package ship CommonJS, ESM, and browser builds under the same import specifier. - Scoped package
- A package whose name starts with
@scope/. Scoped names belong to an npm user or organization. Defaults to private on publish — pass--access publicor setpublishConfig.access. - Workspaces
- A monorepo feature where one root
package.jsonmanages multiple sibling packages by symlinking them undernode_modules/. Supported by npm 7+, pnpm, and yarn. - peerDependency
- A dependency the host project must provide, declared so plugins do not bundle their own copy of a shared library (commonly React, Vue, or a framework core).
Frequently asked questions
Which fields in package.json are required?
Only two fields are strictly required for a package you publish to npm: name and version. The name must be lowercase, URL-safe, and 1–214 characters; version must be a valid semver string (e.g. 1.0.0). For a private project that you do not publish, npm will still accept a package.json without those two fields if you set "private": true — but install, run, and publish commands all rely on them, so most teams set both even for internal repos. The npm docs list dozens of optional fields but the absolute minimum is name + version, or {"private": true} for un-published packages.
What is the difference between dependencies, devDependencies, and peerDependencies?
dependencies are packages required at runtime — they are installed when someone runs npm install on a project that depends on yours. devDependencies are tools used during development (TypeScript, ESLint, test runners) and are NOT installed when your package is added as a dependency. peerDependencies declare packages that the host project must provide — used by plugins (e.g., a React component library declares "react" as a peer so multiple copies of React are not bundled). npm 7+ auto-installs peer dependencies; npm 6 only warned. Use exactly one of the three for each package — never list a package in both dependencies and devDependencies.
What does the "main" field in package.json do?
main points to the CommonJS entry file used when someone calls require("your-package"). Default is index.js if main is omitted. For modern dual-format packages you typically also set "module" (legacy bundler hint), "types" (TypeScript declarations), and "exports" (Node.js 12.7+ conditional exports map). The exports field, when present, takes precedence over main — Node.js will resolve subpath imports only via exports and refuse imports of files not listed there. Most npm packages published after 2023 use exports as the primary entry definition.
What is the difference between "module" and "type": "module" in package.json?
"type" controls the module system Node.js uses for .js files in the package. Set "type": "module" to make .js files ES modules (import/export syntax) and .cjs files CommonJS. Set "type": "commonjs" (or omit it) to make .js files CommonJS and .mjs files ES modules. The "module" field is unrelated — it is a non-standard convention bundlers (webpack, Rollup) use to find the ES module build of a package when a CommonJS-format main also exists. Modern packages use exports.import / exports.require instead of "module".
How do package.json scripts work?
scripts is an object whose keys are arbitrary names you run with npm run <name>, plus a small set of reserved names (start, test, install, prepare, publish — these auto-trigger on lifecycle events). Inside a script, ./node_modules/.bin is added to PATH, so you can call locally-installed tools by name: "test": "jest --coverage". Scripts support pre and post hooks (prebuild runs before build), and you can call other scripts with npm run <name>. From npm 11+, scripts no longer pre-resolve through bash on Windows by default — use cross-env or shx for cross-platform compatibility.
What does the engines field do?
engines lets you declare which Node.js (and optionally npm/pnpm/yarn) versions your package supports. Example: "engines": {"node": ">=20.0.0"}. By default npm only warns when the user runs an incompatible version; setting "engineStrict": true in .npmrc (or "engines-strict=true" in pnpm) turns the warning into an install error. Many CI systems (Vercel, Netlify, Fly) read engines.node to pick the Node version to use during build, so it doubles as deployment configuration. The format follows semver range syntax (^, ~, >=, <).
What is the "files" field in package.json?
files is an allowlist of files and directories that npm includes in the published tarball. If files is missing, npm includes everything except a hardcoded ignore list (node_modules, .git, .gitignore, etc.) plus anything in .npmignore. With files set, only the listed paths plus a few always-included items (package.json, README, LICENSE) are published. Use files to ship only the compiled output (e.g. ["dist", "README.md"]) and keep the source out of the tarball. Run npm pack --dry-run to preview exactly what will be published before running npm publish.
Can package.json have comments?
No — package.json is strict JSON (RFC 8259), which does not allow // or /* */ comments. Adding any comment breaks npm and yarn parsing. The standard workaround is a "//" key that npm intentionally ignores: {"//": "this is a comment", "name": "my-pkg"}. You can also use {"_comment": "..."} but only "//" is officially recognized by npm. For richer commentable configs in the same repo, use a separate file (e.g. JSONC for VS Code settings, .npmrc for npm options). See our JSON Comments guide for the full set of workarounds.
Further reading and primary sources
- npm Docs — package.json — Authoritative npm reference for every field
- Node.js — Packages and modules — Official spec for type, exports, conditional exports, and resolution algorithm
- Semantic Versioning 2.0.0 — The semver specification the version field follows
- pnpm — workspaces — pnpm workspace protocol and filter commands
- browserslist queries — Test browserslist field values against real browser coverage