Skip to main content

What is package-lock.json?

package-lock.json is an automatically generated file that records the exact version of every installed dependency, including transitive ones, so that npm install produces identical results on every machine.

Theory

TL;DR

  • Analogy: package.json is a recipe saying "use flour, version 4.x." package-lock.json is the receipt: "batch 4.17.21 from supplier X."
  • Main difference: package.json allows version ranges for flexibility; package-lock.json pins the exact resolved tree.
  • npm ci reads the lockfile strictly and fails on any mismatch. npm install may update versions within allowed ranges.
  • Always commit package-lock.json to git. Never add it to .gitignore.

Quick example

bash
# package.json allows a range echo '{"dependencies":{"lodash":"^4.17.0"}}' > package.json npm install # Installs lodash@4.17.21, creates package-lock.json # On another machine or in CI: npm ci # Installs exactly lodash@4.17.21, not whatever is latest now

npm ci deletes node_modules first, then restores from the lockfile. No surprises.

What the lockfile actually stores

The lockfile records the resolved version, registry URL, and a SHA-512 integrity hash for each package. When you run npm ci, npm verifies those hashes against the downloaded tarballs. If a hash does not match, the install fails.

A simplified entry looks like this:

json
"express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", "integrity": "sha512-..." }

This is the v3 format, introduced in npm 7. It includes all hoisted packages and their transitive dependencies in a flat structure.

Key difference

package.json says "I need express at roughly 4.18.x." The registry can satisfy that with 4.18.2 today and 4.18.5 next month. package-lock.json records which one actually got installed. Without the lockfile, two developers running npm install a week apart may end up with different versions of a transitive dependency they never explicitly added. One of them gets a production bug.

When to use

  • Team project: commit the lockfile, run npm ci in CI/CD pipelines.
  • Production Dockerfile: RUN npm ci --omit=dev, not RUN npm install.
  • Solo prototype: commit it anyway. Future you will thank present you.
  • Updating a specific package: use npm update lodash. Do not delete the lockfile.

Common mistakes

1. Adding package-lock.json to .gitignore

This is the most common source of "works on my machine" bugs. Every developer ends up with slightly different transitive dependencies.

bash
# Wrong: package-lock.json in .gitignore # Developer A gets lodash@4.17.21 # Developer B gets lodash@4.17.22 a week later (different behavior in one edge case)

Fix: remove from .gitignore and commit.

2. Using npm install in CI instead of npm ci

dockerfile
# Wrong RUN npm install # Updates lockfile if ranges allow newer versions # Correct RUN npm ci --omit=dev

npm install may pull a newer version automatically. npm ci treats any divergence as a hard error and stops the build.

3. Deleting the lockfile to resolve merge conflicts

bash
rm package-lock.json && npm install # Resolves everything from scratch

This pulls the latest allowed version of every package in your ranges. You lose the dependency history and may introduce regressions. Use npm install <package> or npm dedupe for targeted fixes instead.

4. Mixing package managers in one project

If the project has package-lock.json, running yarn install to add a package creates two conflicting lockfiles with different resolved trees. Pick one package manager and stick with it.

Real-world usage

I saw a production incident where a transitive dependency released a minor version with a broken JSON parser. The team had package-lock.json committed, but used npm install in their pipeline, so the update slipped through. Switching to npm ci caught the same class of problem on the next occurrence.

  • React apps: locks react@18.2.0 and scheduler@0.23.0 together for consistent rendering.
  • Express servers: pins helmet@7.1.0 so a security middleware update does not change behavior mid-deploy.
  • Next.js: locks webpack@5.89.0 to keep build output predictable.
  • Docker builds: npm ci in the Dockerfile ensures the image matches what was tested locally.

Follow-up questions

Q: What is the difference between npm install and npm ci?
A: npm install creates or updates the lockfile and installs packages. npm ci deletes node_modules, reads the lockfile without modifying it, and fails if package.json and the lockfile are out of sync. Use npm ci for CI/CD and Docker.

Q: Should you commit package-lock.json for a library (not an app)?
A: No. Libraries should only commit package.json and let consumers resolve their own trees. Committing a lockfile for a library forces specific transitive versions on every developer who installs it.

Q: What happens if the lockfile's integrity hash does not match the registry?
A: npm re-fetches the tarball using the resolved URL and re-checks the SHA-512. If it still does not match, the install fails with an integrity error. This protects against tampered packages.

Q: In a monorepo with npm workspaces, is there one lockfile or many?
A: One root-level package-lock.json. npm hoists shared dependencies to the root node_modules and records the full workspace tree in that single file.

Examples

Locking dependencies for an Express app

json
// package.json { "dependencies": { "express": "^4.18.0", "helmet": "^7.0.0" } }
bash
npm install # package-lock.json now records: # express@4.18.2 with its full transitive tree # helmet@7.1.0 with its full transitive tree

The caret range in package.json allows anything from 4.18.0 up to (not including) 5.0.0. The lockfile picks one version and holds it. When 4.19.0 ships next month, npm ci still installs 4.18.2 on every machine.

npm ci in a production Dockerfile

dockerfile
FROM node:20-alpine WORKDIR /app # Copy package files before source code COPY package.json package-lock.json ./ # Install exact versions from lockfile, skip dev dependencies RUN npm ci --omit=dev COPY . . CMD ["node", "index.js"]

Copying package.json and package-lock.json before the rest of the source code lets Docker cache the npm ci layer. If only source files change, Docker reuses the cached node_modules and the build runs faster. If either package file changes, Docker invalidates the cache and reinstalls from scratch.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?