Versions, lockfiles and npm ci

Node.js · Essentials · 10 min

Reproducible installs prevent "works on my machine" bugs and surprise breakages from transitive updates.

npm packages use semantic versioning: MAJOR.MINOR.PATCH. A major bump may break you, a minor adds features compatibly, a patch only fixes bugs. In package.json you declare a range, not an exact version:

  • ^1.4.2 — any 1.x.y at or above 1.4.2 (the default npm writes). For 0.x versions the caret is stricter: ^0.4.2 means >=0.4.2 <0.5.0.
  • ~1.4.2 — patch updates only: >=1.4.2 <1.5.0.
  • 1.4.2 — exactly that version.

Ranges alone would make every install potentially different, which is why npm writes package-lock.json. It records the exact version, download URL and integrity hash of every package in the tree, including dependencies of dependencies. Commit it.

Two install commands, two purposes:

  • npm install — resolves ranges, may update the lockfile. Use it when adding or upgrading packages.
  • npm ci — installs exactly what the lockfile says, deletes node_modules first, and fails if package.json and the lockfile disagree. Use it in CI and Docker builds.

To maintain dependencies:

  • npm outdated lists packages with newer versions.
  • npm update upgrades within your ranges; a major upgrade needs npm install pkg@latest and reading the changelog.
  • npm ls pkg shows why a package is in your tree.
  • npm audit reports known vulnerabilities.

Every dependency is code you run with full privileges. Prefer fewer, well-maintained packages, and check the weekly downloads and maintenance status before adding one.

Example

# package.json ranges
#   "express": "^5.1.0"   -> 5.1.0 up to (not including) 6.0.0
#   "zod":     "~3.24.1"  -> 3.24.x only
#   "uuid":    "11.0.3"   -> exactly 11.0.3

npm install            # resolve ranges, update package-lock.json if needed
git add package.json package-lock.json

# In CI / Dockerfile: exact, clean, fails on mismatch
npm ci

npm outdated           # what is behind?
npm install express@latest   # deliberate major upgrade
npm ls debug           # which packages pull in 'debug'?
npm audit              # known vulnerabilities

Remember

  • ^ allows minor and patch updates, ~ only patches
  • Commit package-lock.json; it pins the whole dependency tree
  • Use npm ci for reproducible builds

Watch out for

  • Deleting package-lock.json to "fix" an install, which silently upgrades the whole tree.

Check yourself

  1. package.json says "lib": "^2.3.0". Which version could npm install install?
  2. Why use npm ci rather than npm install in a CI pipeline?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.npmjs.com/about-semantic-versioning