Last updated: 2026-09-14

package.json

package.json is the manifest file at the root of every Node.js project. It describes what the project is, what it depends on, how to run it, and a range of other metadata that both npm and other tools rely on. If node_modules is where your dependencies physically live, package.json is the source of truth that says which dependencies should be there in the first place.

Every npm command covered in the previous topic — installing, removing, updating, running scripts — reads from or writes to this file. Understanding its structure in depth is what makes those commands fully legible.


A Complete Example

{
  "name": "winehouse-labs-api",
  "version": "1.2.0",
  "description": "Backend API for WinehouseLabs",
  "main": "server.js",
  "type": "module",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "test": "jest"
  },
  "keywords": ["api", "education", "node"],
  "author": "Wariz",
  "license": "MIT",
  "dependencies": {
    "express": "^4.18.2",
    "dotenv": "^16.4.5"
  },
  "devDependencies": {
    "nodemon": "^3.1.0",
    "jest": "^29.7.0"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}

Each field has a specific purpose. The most important ones for day-to-day work are covered below.


Core Identity Fields

{
  "name": "winehouse-labs-api",
  "version": "1.2.0",
  "description": "Backend API for WinehouseLabs"
}

name identifies the project. If the package is ever published to the npm registry, this must be unique across the entire registry. For a private application that will never be published, the name just needs to be valid — lowercase, no spaces, hyphens allowed.

version follows semantic versioning, covered in the previous topic. For an application that is not published as a reusable package, this version number is mostly informational, though some deployment tools and changelogs still rely on it.

description is a short, plain-text summary of what the project does.


main — the Entry Point

{
  "main": "server.js"
}

main specifies which file is loaded when another piece of code does require('your-package-name'). This matters primarily for packages published to npm and intended to be imported by other projects. For a standalone application — like a typical Node.js or Express backend — main has limited practical effect, but it remains a useful piece of documentation indicating the application's true entry point.


type — CommonJS or ES Modules

{
  "type": "module"
}

This field, covered in detail in the CommonJS vs ES Modules topic, determines how Node.js interprets .js files in the project. Setting "type": "module" treats every .js file as an ES Module, enabling import/export syntax. Omitting this field, or setting it to "commonjs" explicitly, keeps the default CommonJS behaviour using require()/module.exports.


scripts — Named Command Shortcuts

{
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "test": "jest",
    "build": "tsc",
    "lint": "eslint .",
    "format": "prettier --write ."
  }
}

As covered in the npm Basics topic, each entry here becomes runnable with npm run <name> (or just npm start / npm test for those two specific names). Scripts can call any installed CLI tool, any locally installed package's binary, or chain multiple commands together.

Chaining Commands

Scripts can run multiple commands in sequence using &&:

{
  "scripts": {
    "build": "npm run lint && npm run test && tsc"
  }
}

This runs linting, then tests, then the TypeScript build — and if any step fails, the chain stops at that point rather than continuing.

Lifecycle Scripts

npm recognises certain script names as lifecycle hooks — scripts that run automatically at specific points, without being called directly:

Script nameRuns automatically when
preinstallBefore npm install runs
postinstallAfter npm install completes
pretestBefore npm test
posttestAfter npm test
{
  "scripts": {
    "postinstall": "echo 'Dependencies installed successfully'"
  }
}

Any script name prefixed with pre or post matching an existing script name runs automatically around it. This is commonly used for build steps that must happen after dependencies are installed, such as compiling native code or running a database migration.


dependencies and devDependencies

{
  "dependencies": {
    "express": "^4.18.2",
    "dotenv": "^16.4.5"
  },
  "devDependencies": {
    "nodemon": "^3.1.0",
    "jest": "^29.7.0"
  }
}

dependencies lists packages required for the application to actually run — Express, database drivers, anything imported by the production code.

devDependencies lists packages only needed during development — testing frameworks, linters, dev-time tooling like nodemon. These are not installed when a production deployment runs npm install --production or npm ci --production.


Version Range Syntax

Each dependency's version is not usually pinned to one exact version — it is typically expressed as a range, indicating which updates are considered acceptable.

PrefixMeaningExampleAllows
(none)Exact version only4.18.2Only 4.18.2
^Compatible with — allows minor and patch updates^4.18.24.18.3, 4.19.0, up to but not including 5.0.0
~Approximately — allows only patch updates~4.18.24.18.3, 4.18.9, but not 4.19.0
>=Greater than or equal to>=4.18.2Any version 4.18.2 or newer
*Any version*Any version at all — rarely used deliberately

^ is npm's default when installing a package, and it is the most commonly used prefix. It reflects the semantic versioning convention covered previously — MINOR and PATCH updates are expected to be backward compatible, so allowing them automatically is generally safe, while MAJOR updates may include breaking changes and should be reviewed deliberately before upgrading.

npm install express        # adds "express": "^4.18.2" — caret range
npm install express --save-exact  # adds "express": "4.18.2" — exact version

package-lock.json

package.json specifies acceptable version ranges, but it does not record the exact version actually installed, nor does it record the exact versions of every nested dependency several layers deep. package-lock.json does both.

{
  "name": "winehouse-labs-api",
  "lockfileVersion": 3,
  "packages": {
    "node_modules/express": {
      "version": "4.18.2",
      "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
      "integrity": "sha512-...",
      "dependencies": { ... }
    }
  }
}

This file is generated and updated automatically by npm whenever install runs. It guarantees that everyone working on the project — and every deployment of the project — installs the exact same dependency tree, down to every nested dependency, regardless of when npm install is run or which version ranges happen to resolve differently over time.

Why package-lock.json Should Be Committed

Unlike node_modules, package-lock.json should be committed to version control. It is small, it is plain text, and it is what makes builds reproducible. Without it, two different developers running npm install on the same package.json at different times could end up with subtly different dependency versions — including different versions of nested dependencies neither of them explicitly chose — leading to bugs that are difficult to reproduce because they only occur on one machine.

# .gitignore
node_modules/
# package-lock.json should NOT be in .gitignore

npm ci vs npm install

npm ci

npm ci ("clean install") is a stricter alternative to npm install, intended for automated environments — CI/CD pipelines, deployment processes. It installs exactly what is specified in package-lock.json, fails immediately if package.json and package-lock.json are out of sync, and always deletes node_modules first for a guaranteed clean state. It is faster than npm install in these contexts and is the recommended command for deployment scripts, while npm install remains the right choice for everyday local development.


engines — Specifying Required Node.js Versions

{
  "engines": {
    "node": ">=18.0.0",
    "npm": ">=9.0.0"
  }
}

engines documents which Node.js (and optionally npm) versions the project is built for and expected to run on. By default, npm only warns rather than blocks installation if the current environment does not match, though this can be configured to fail strictly. It is good practice to set this honestly, particularly for projects that rely on features only available in newer Node.js versions, such as native .env file support or import.meta.dirname.


Other Common Fields

{
  "private": true,
  "repository": {
    "type": "git",
    "url": "https://github.com/wariz007/winehouse-labs-api.git"
  },
  "license": "MIT"
}

private: true prevents the package from ever being accidentally published to the public npm registry — an important safeguard for internal applications that should never end up publicly listed.

repository links to the project's source control location, used by the npm registry's website to display a link back to the source for published packages.

license declares the legal terms under which the code can be used — MIT is among the most common for open-source projects, due to its permissiveness.


Summary

FieldPurpose
name / version / descriptionCore project identity
mainEntry point file, relevant mainly for published packages
type"module" for ES Modules, omitted or "commonjs" for CommonJS
scriptsNamed command shortcuts, runnable with npm run
dependenciesPackages required to run the application
devDependenciesPackages only needed during development
enginesDocuments required Node.js/npm versions
privatePrevents accidental publishing to the npm registry
Version prefix ^Allows MINOR and PATCH updates
Version prefix ~Allows PATCH updates only
package-lock.jsonRecords exact installed versions; should be committed
npm ciStrict, reproducible install used in deployment and CI