Understanding package-lock.json
package.json records the versions you want — usually as flexible ranges like ^4.19.2. But "compatible with 4.19.2" can resolve to different actual versions on different days. package-lock.json removes that uncertainty: it records the exact version of every package in your tree, so every npm install, on every machine, reproduces a byte-identical node_modules.
The problem it solves
Imagine your package.json says "express": "^4.19.2". The caret allows any 4.x ≥ 4.19.2. Without a lock, here is what happens over time:
Same package.json, different results
Monday — you install → express 4.19.2 resolves
Friday — CI installs → express 4.20.0 just published → CI gets 4.20.0
"works on my machine" — but not in CIThe lockfile pins the resolution. Commit it, and Monday-you and Friday-CI install the same 4.19.2 until someone deliberately updates.
What it records
Field | Meaning |
|---|---|
| The exact resolved version (no range) |
| The precise URL the tarball came from |
| A Subresource-Integrity hash (SHA-512) of the tarball |
| Flags marking why the package is in the tree |
| The lockfile format version (3 since npm 7) |
A single entry in package-lock.json (lockfileVersion 3)
"node_modules/express": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-N9...long-hash...==",
"dependencies": { "accepts": "~1.3.8", "body-parser": "1.20.2" }
}npm install vs npm ci
These two commands treat the lockfile very differently. Knowing which to use where is the single most important npm-reproducibility habit:
|
| |
|---|---|---|
Reads lockfile | Yes, but may update it | Yes — treats it as read-only |
Lockfile out of sync with package.json | Rewrites the lockfile | Errors out |
Existing node_modules | Reconciles in place | Deletes and reinstalls from scratch |
Can add/remove single packages | Yes ( | No — installs the whole tree |
Use it for | Local dev, adding deps | CI, Docker builds, production |
npm ci # clean, lockfile-exact, fails fast if drift — perfect for CI
Always commit it
Commit
package-lock.jsonto git — it is part of your source of truth, like the manifest itself.Never edit it by hand — let npm regenerate it. Manual edits are error-prone and easily inconsistent.
Resolve merge conflicts by re-running
npm install(it rebuilds the lock frompackage.json), not by hand-merging the JSON.Review lockfile diffs in PRs — an unexpected change to a transitive dependency can be the first sign of a supply-chain issue.
Inspecting and fixing the tree
npm ls express # show where a version came from in the tree npm ls --all # the full resolved tree npm dedupe # flatten duplicate packages where ranges allow rm -rf node_modules package-lock.json && npm install # nuke & rebuild