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 cireads the lockfile strictly and fails on any mismatch.npm installmay update versions within allowed ranges.- Always commit package-lock.json to git. Never add it to .gitignore.
Quick example
# 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 nownpm 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:
"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 ciin CI/CD pipelines. - Production Dockerfile:
RUN npm ci --omit=dev, notRUN 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.
# 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
# Wrong
RUN npm install # Updates lockfile if ranges allow newer versions
# Correct
RUN npm ci --omit=devnpm 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
rm package-lock.json && npm install # Resolves everything from scratchThis 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.0andscheduler@0.23.0together for consistent rendering. - Express servers: pins
helmet@7.1.0so a security middleware update does not change behavior mid-deploy. - Next.js: locks
webpack@5.89.0to keep build output predictable. - Docker builds:
npm ciin 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
// package.json
{
"dependencies": {
"express": "^4.18.0",
"helmet": "^7.0.0"
}
}npm install
# package-lock.json now records:
# express@4.18.2 with its full transitive tree
# helmet@7.1.0 with its full transitive treeThe 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
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 readyA concise answer to help you respond confidently on this topic during an interview.