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.

Validate package.json

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.

FieldInstalled whenBundled to consumers?Typical use
dependenciesAlways (production install)Yes — transitivelyRuntime libraries (express, axios)
devDependenciesOnly on npm install in your repoNoBuild tools, test runners, types
peerDependenciesHost project provides (auto-installed by npm 7+)NoPlugins, React components, frameworks
optionalDependenciesBest-effort; install failure is non-fatalIf installedNative 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.

FieldResolves forWhen to set
mainrequire() (CommonJS)Always, for backward compat
moduleBundler hint for ES module buildLegacy bundlers; superseded by exports
types (or typings)TypeScript type lookupIf you ship .d.ts files
exportsModern resolver (Node 12.7+, bundlers)All new packages — controls what is importable
binCLI 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 read engines.node to pick the build runtime. Pair with engine-strict=true in .npmrc to 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 accidental npm publish.
  • publishConfig — overrides per-publish settings. access: public is 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.

ErrorCauseFix
Unexpected token } in JSONTrailing comma after last itemRemove the trailing comma; use a JSON validator
Invalid name: must not contain capital lettersUppercase letter in nameLowercase the name; npm enforces this
Invalid version: ...Non-semver version like 1.0Use 1.0.0 — three numeric segments required
EPUBLISHCONFLICT on npm publishVersion already publishedBump version: npm version patch (or minor/major)
EUSAGE Cannot read property 'replace' of undefinedMissing required field (often name) or malformed exports mapRe-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 exports map 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 public or set publishConfig.access.
Workspaces
A monorepo feature where one root package.json manages multiple sibling packages by symlinking them under node_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