package.json Fields

beginner package-json npm dependencies exports

package.json is the heart of any Node project. In simple language — it tells Node and npm everything about our project: name, version, what to run, what to install, and how others should import from us.

A real-world example:

{
  "name": "khoj",
  "version": "1.2.0",
  "description": "Personal job scraper",
  "type": "module",
  "main": "./dist/index.js",
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils.js"
  },
  "scripts": {
    "dev": "node --watch src/index.js",
    "test": "node --test",
    "build": "tsc"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "pg": "^8.11.0"
  },
  "devDependencies": {
    "typescript": "^5.3.0"
  },
  "engines": {
    "node": ">=20"
  }
}

name and version

name is how npm and require find our package. Lowercase, no spaces, optionally scoped (@scope/name).

version follows semverMAJOR.MINOR.PATCH. Increment major for breaking changes, minor for new features, patch for fixes.

type — CJS or ESM?

  • "type": "module" → all .js files are ESM
  • "type": "commonjs" (or absent) → all .js files are CJS

This setting controls how Node loads our files. See the CommonJS vs ESM note for details.

main, module, exports — the entry points

These three control what consumers get when they import or require our package.

  • main — the classic entry. Used by CJS require() and as the fallback.
  • module — bundler-only field (Webpack, Rollup). Points to an ESM build. Node ignores this.
  • exports — modern, strict, conditional. Beats main if present.
{
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./package.json": "./package.json"
  }
}

With exports, anything not listed is blockedrequire("my-pkg/internal/secret") throws. This is intentional; it gives us module encapsulation.

scripts — our project’s commands

Anything in scripts runs via npm run <name> (or yarn <name>, pnpm <name>). A few names are special:

  • start — runs with just npm start
  • test — runs with just npm test
  • pre<x> / post<x> — auto-run before/after script <x>
{
  "scripts": {
    "dev": "node --watch src/index.js",
    "build": "tsc -p tsconfig.json",
    "test": "node --test test/",
    "lint": "eslint src/",
    "prebuild": "rm -rf dist"
  }
}

npm run sets node_modules/.bin on the PATH, so we can invoke locally-installed CLIs like tsc or eslint without a global install.

The three dependency buckets

dependencies
Needed at runtime. Installed when someone installs our package. Example: express, axios.
devDependencies
Needed only during development. Skipped with npm install --production. Example: typescript, eslint, vitest.
peerDependencies
"I work with this — please provide it." Common in plugins (eslint plugins, react libraries). Not auto-installed in npm v7+ for legacy reasons but they are by default in modern npm.
{
  "dependencies": {
    "express": "^4.18.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.0",
    "typescript": "^5.0.0"
  },
  "peerDependencies": {
    "react": ">=18"
  }
}

Version range syntax

  • ^1.2.3 — compatible (≥1.2.3, <2.0.0). The default with npm install.
  • ~1.2.3 — patch-level only (≥1.2.3, <1.3.0).
  • 1.2.3 — exact.
  • * or latest — anything (don’t do this).
  • >=1.2.3 <2.0.0 — explicit range.

engines — declare runtime requirements

Tells installers what Node version we need. Without engine-strict it’s just a warning, but it shows up in errors and documents intent:

{
  "engines": {
    "node": ">=20.0.0",
    "npm": ">=10.0.0"
  }
}

Many CI systems and platforms (Vercel, Render) read this to pick the right Node version.

Other useful fields

  • bin — declare CLI executables. npm install -g my-cli puts these on PATH.
  • files — whitelist of files to publish. Without it, npm uses .npmignore or includes everything.
  • workspaces — array of paths or globs for monorepo sub-packages.
  • private: true — prevents accidental npm publish.
  • sideEffects: false — bundler hint for tree-shaking. Means “imports of this package have no side effects, drop unused exports”.
{
  "bin": {
    "my-cli": "./cli.js"
  },
  "files": ["dist/", "README.md"],
  "private": true,
  "sideEffects": false
}

Generating it

npm init -y makes a minimal one. From there, every npm install <pkg> updates dependencies automatically.