Back to core workflows Fix dependency resolution Tune package metadata Jump to monorepo patterns

Understanding package.json Fields

The package.json manifest is the single contract between your source tree, the resolver, the bundler, and every consumer who installs your package — and a single misordered field can break all four without throwing an error locally. This page is a field-by-field reference for the manifest, organized by the job each group of fields performs: identity, module resolution, dependency classification, and workspace orchestration. It is part of the broader Core JavaScript Package Workflows, and the diagram below shows which resolver or tool reads each group of fields.

Which tool reads which package.json field group The manifest at the center feeds identity fields to the registry, exports to the resolver, dependency fields to the installer, and workspace fields to the task runner. package.json the manifest registry name, version, license resolver / bundler exports, type, types installer deps, peers, overrides task runner workspaces, engines
One manifest, four readers: identity fields go to the registry, exports to the resolver, dependency fields to the installer, and workspace fields to the task runner.

Core Metadata and Package Identity

Establish strict package identity using an RFC 1123-compliant name and a SemVer 2.0.0 version. Mandate SPDX-compliant license strings to satisfy automated compliance scanners, since a non-SPDX value such as a freeform "MIT-ish" will be rejected by license-policy gates. For monorepo roots, enforce private: true to prevent accidental publication of internal scaffolding — this is the single most effective guard against leaking a private repository to a public registry, because npm publish refuses to run on a private manifest entirely.

Implementation Checklist

  • Validate name against registry availability and npm naming rules (lowercase, no spaces, URL-safe; scoped names take the form @scope/name).
  • Pin version to exact SemVer; strip pre-release tags (-alpha, -rc) before publishing to the latest dist-tag.
  • Set license to a valid SPDX identifier (MIT, Apache-2.0, BSD-3-Clause).
  • Apply "private": true at the monorepo root to block npm publish execution.
# Verify package name availability before commit
npm view <package-name> version 2>/dev/null || echo "Name available"

# Validate version format
node -e "const v=require('./package.json').version; if(!/^\d+\.\d+\.\d+/.test(v)) process.exit(1)"

# Confirm private flag at root
node -e "if(!require('./package.json').private){console.error('Missing private:true');process.exit(1);}"

Module Resolution and Export Maps

Configure explicit exports maps to replace the legacy main and module fields. Define conditional exports (types, import, require, default) to guarantee deterministic resolution across bundlers and runtimes. The resolver walks the conditions top to bottom and uses the first match, so the order of keys is load-bearing: place types first so a TypeScript consumer resolves declarations before any JavaScript condition is considered, and place default last as the catch-all. Once an exports map exists, any subpath you do not list becomes unreachable, which is what makes the map an encapsulation boundary rather than just a router.

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js",
      "default": "./dist/esm/index.js"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/esm/utils.js",
      "require": "./dist/cjs/utils.js"
    },
    "./package.json": "./package.json"
  }
}

Shipping two JavaScript formats from one manifest is the most error-prone part of this field, so the complete recipe — including the per-format types nesting and matching build output — is broken out in How to Configure package.json for Dual Modules. The underlying loader rules that the import and require conditions exist to satisfy are the subject of ESM and CJS Interoperability, and the types condition specifically must point at format-correct declarations or a consumer hits a "cannot find module" error, which is covered in TypeScript Declaration Publishing. The manifest you finalize here is exactly what gets validated and uploaded by the npm Registry Publishing Workflows.

# Verify ESM resolution locally
node -e "import('./dist/esm/index.js').then(() => console.log('ESM OK'))"

# Verify CJS resolution locally
node -e "require('./dist/cjs/index.js'); console.log('CJS OK')"

Dependency Classification and Security Overrides

Differentiate dependencies, devDependencies, and peerDependencies to minimize the production install footprint and prevent runtime duplication. Runtime imports belong in dependencies; anything used only to build or test belongs in devDependencies; a framework the consumer already owns belongs in peerDependencies. Declaring a peer such as react as a direct dependency is the classic cause of two framework copies in one tree, which the broader Dependency Resolution Explained topic dissects in full.

Use overrides (npm v8.3+), pnpm.overrides, or resolutions (Yarn v1) to patch vulnerable transitive dependencies deterministically. Keep overrides narrow and audited; a broad override can silently pin an incompatible major version into a sub-tree you never inspect. Align this pinning with strict Lockfile Management Strategies so the patched versions are recorded and verified on every install rather than re-resolved.

{
  "dependencies": { "lodash-es": "^4.17.21" },
  "devDependencies": { "typescript": "^5.7.0", "vitest": "^3.0.0" },
  "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" },
  "peerDependenciesMeta": { "react": { "optional": true } },
  "overrides": { "semver": "^7.5.4", "postcss": "^8.4.35" }
}

The CI workflow below installs with a frozen lockfile, runs an audit gate, and fails if the install mutated the lockfile — the three checks that together keep the dependency fields in this manifest honest.

# .github/workflows/dependency-audit.yml
name: Dependency & Lockfile Guard
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 10
      - run: pnpm install --frozen-lockfile
      - run: pnpm audit --audit-level=high
      - name: Verify no lockfile drift
        run: git diff --exit-code pnpm-lock.yaml

Workspace Orchestration and Engine Constraints

Define a workspaces array (npm and Yarn) or a pnpm-workspace.yaml to enable local linking, controlled hoisting, and cross-package script execution. Configure engines to enforce Node.js and package manager versions, and pair it with packageManager so Corepack activates the exact tool. With --engine-strict, an install on an unsupported Node version fails immediately rather than producing a subtly broken tree.

{
  "engines": { "node": ">=20.0.0", "pnpm": ">=10.0.0" },
  "packageManager": "pnpm@10.4.1",
  "overrides": { "semver": "^7.5.4" },
  "workspaces": ["packages/*", "apps/*"]
}

How these fields drive topological builds, hoisting controls, and filtered execution is the focus of the Workspace Configuration Deep Dive.

# Enforce package manager version via corepack
corepack enable
corepack prepare pnpm@10.4.1 --activate

# Run workspace-aware scripts
pnpm --filter "@scope/ui" build
pnpm -r --parallel test

# Validate engine constraints before install
pnpm install --engine-strict

Common Anti-Patterns

Anti-Pattern Impact Remediation
Using main/module alongside exports Bundler resolution conflicts, broken tree-shaking Delete main/module; rely on exports, keeping only a main fallback for pre-exports toolchains
Omitting types in conditional exports TS consumers fall back to implicit @types or fail outright Always declare "types" first in each export branch
Leaving private unset at the monorepo root Accidental npm publish of scaffolding or CI secrets Set "private": true in the root package.json
Using ^/~ for peerDependencies Incompatible runtime versions in consumer apps Use explicit minimums such as ">=18.0.0"
Hardcoding file: paths in dependencies Broken CI installs, non-portable graphs Use the workspace protocol: "workspace:*"

Frequently Asked Questions

Should I use main or exports for modern package distribution? Always prioritize exports. It provides explicit, secure resolution paths, hides internal files, and supports conditional loading for ESM, CJS, and types. Keep main only as a fallback for tooling that predates the exports map.

How do I enforce strict dependency versions across a monorepo? Use overrides (npm) or resolutions (Yarn) at the root package.json to force specific transitive versions, then combine that with engines constraints and a frozen lockfile so the pins are verified on every install.

What is the correct way to handle peerDependencies in library authoring? Declare peers with explicit minimum versions (e.g., "react": ">=18.0.0") rather than caret/tilde ranges, and use peerDependenciesMeta with optional: true for integrations that should not block installs when the peer is absent.

Why does field order matter inside an exports map? Node and TypeScript use the first matching condition, so a require listed before types would resolve JavaScript before declarations. Keep types first and default last in every branch.

Related

Core JavaScript Package Workflows