Skip to content
Node.js nd modules 4 min read

The package.json File Explained

Every Node.js project of any size has a package.json at its root. It is the manifest that describes your package to Node, to npm, and to anyone who installs your code: its name and version, how to run it, which module system it uses, and what it depends on. Understanding each field lets you ship libraries, build reproducible apps, and avoid a whole class of resolution and tooling surprises.

Creating a package.json

You rarely write package.json by hand from scratch. The npm init command scaffolds one interactively, and the -y flag accepts all defaults for a quick start.

npm init -y

Output:

Wrote to /home/dev/my-app/package.json:

{
  "name": "my-app",
  "version": "1.0.0",
  "main": "index.js",
  "type": "commonjs"
}

Identity fields: name and version

The name and version fields together uniquely identify a release on the npm registry. name must be lowercase, URL-safe, and may be scoped (@acme/utils). version must be a valid semantic version string — MAJOR.MINOR.PATCH. If you never publish, these fields are still required for npm to function, but their values are largely cosmetic.

{
  "name": "@acme/data-tools",
  "version": "2.4.1"
}

If your package is private and should never be published, add "private": true. npm will refuse to publish it, protecting you from accidental npm publish leaks.

Choosing a module system: type

The type field tells Node how to interpret .js files in your package. The two valid values are "commonjs" (the default when omitted) and "module". With "module", every .js file is treated as an ES module and uses import/export; with "commonjs", files use require/module.exports. You can always override per-file with the .mjs (always ESM) and .cjs (always CommonJS) extensions.

{
  "type": "module"
}

Entry points: main, module, and exports

These fields declare what consumers get when they import your package.

FieldPurposeNotes
mainClassic entry pointUsed by older tooling and CommonJS require
moduleESM entry hintA convention honored by bundlers, not by Node itself
exportsModern entry mapTakes precedence over main; can gate files and provide conditional ESM/CJS builds

The exports field is the modern, recommended way to define entry points. It encapsulates your package — anything not listed cannot be imported — and supports conditional resolution so you can ship both ESM and CommonJS builds from one package.

{
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./package.json": "./package.json"
  }
}

Once you add an exports map, deep imports like import x from 'pkg/dist/internal.js' stop working unless you list those subpaths explicitly. This is a feature: it lets you keep internals private.

scripts

The scripts field defines named commands you run with npm run <name>. A few names are special and run on lifecycle events — start, test, prepare, and the pre/post prefixes (e.g. prebuild runs before build). Scripts execute with node_modules/.bin on the PATH, so locally installed CLIs are callable by name.

{
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js",
    "test": "node --test",
    "build": "tsc -p tsconfig.json",
    "prebuild": "rimraf dist"
  }
}
npm run dev
npm test

Dependency fields

npm distinguishes between several dependency buckets so installs stay lean and predictable.

FieldInstalled whenUse for
dependenciesAlways (including by consumers)Runtime requirements your code imports
devDependenciesOnly in the project itself, not by consumersTest runners, bundlers, linters, types
peerDependenciesNot auto-installed; expected in hostPlugins that must share the host’s copy (e.g. a React component lib expecting react)
optionalDependenciesAttempted, ignored on failurePlatform-specific or nice-to-have packages

Version ranges use semver operators: ^1.2.3 allows compatible updates (>=1.2.3 <2.0.0), ~1.2.3 allows patch updates only, and an exact 1.2.3 pins precisely.

{
  "dependencies": {
    "express": "^4.19.2"
  },
  "devDependencies": {
    "typescript": "^5.5.4",
    "@types/node": "^22.5.0"
  },
  "peerDependencies": {
    "react": ">=18"
  }
}

engines and bin

The engines field declares which Node (or npm) versions your package supports. By default it is advisory — installs still succeed — but you can enforce it with an .npmrc containing engine-strict=true.

{
  "engines": {
    "node": ">=20.0.0"
  }
}

The bin field exposes executable commands. When your package is installed globally or its bin is run via npx, npm creates a symlink on the PATH pointing to the listed file. The target file should start with a shebang (#!/usr/bin/env node).

{
  "bin": {
    "my-cli": "./bin/cli.js"
  }
}

A realistic example

Putting it together, here is a small ESM library that also ships a CLI.

{
  "name": "@acme/greet",
  "version": "1.0.0",
  "description": "Friendly greeting utilities",
  "type": "module",
  "exports": {
    ".": "./src/index.js"
  },
  "bin": {
    "greet": "./bin/greet.js"
  },
  "scripts": {
    "test": "node --test",
    "lint": "eslint src"
  },
  "engines": {
    "node": ">=20.0.0"
  },
  "dependencies": {
    "kleur": "^4.1.5"
  },
  "devDependencies": {
    "eslint": "^9.9.0"
  },
  "license": "MIT"
}

Best Practices

  • Set "type": "module" for new projects and use .cjs only where a dependency forces CommonJS.
  • Prefer the exports map over main to control your public surface and ship dual ESM/CJS builds.
  • Add "private": true to apps you never intend to publish to avoid accidental publishes.
  • Keep build, test, and lint tools in devDependencies so consumers don’t download them.
  • Use peerDependencies for plugins so the host application controls the shared version.
  • Declare engines to document supported Node versions, and enable engine-strict in CI if you need it enforced.
  • Always commit your package-lock.json alongside package.json for reproducible installs.
Last updated June 14, 2026
Was this helpful?