NodeJSpackage-lock.json & Lockfiles

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

Text
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 CI

The 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

version

The exact resolved version (no range)

resolved

The precise URL the tarball came from

integrity

A Subresource-Integrity hash (SHA-512) of the tarball

dev / optional

Flags marking why the package is in the tree

lockfileVersion

The lockfile format version (3 since npm 7)

A single entry in package-lock.json (lockfileVersion 3)

JSON
"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" }
}
`integrity` is a tamper check
The `integrity` hash is verified on every install. If a tarball at `resolved` does not match the recorded SHA-512 — because it was corrupted in transit or maliciously replaced — npm aborts. This is your defense against a compromised registry or proxy serving altered code.
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:

npm install

npm ci

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 (npm i pkg)

No — installs the whole tree

Use it for

Local dev, adding deps

CI, Docker builds, production

Bash
npm ci      # clean, lockfile-exact, fails fast if drift — perfect for CI
Use `npm ci` in CI and Docker — always
`npm install` can silently *change* your lockfile mid-build (e.g. when a transitive range allows a newer version), so two CI runs from the same commit may differ. `npm ci` refuses to start unless `package.json` and `package-lock.json` already agree, then builds exactly what the lock says. It is also faster because it skips dependency resolution.
Always commit it
  • Commit package-lock.json to 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 from package.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.

Libraries publish without it — apps depend on it
When you `npm publish` a library, the lockfile is *not* included in the tarball: your consumers resolve their own tree against *their* dependency set. The lockfile governs *your* repository's installs (dev, CI, deploy). So: commit it for apps and libraries alike, but understand it only locks **your** builds, not your users'.
Inspecting and fixing the tree

Bash
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
Next
Now see the install command itself in depth — flags, where files land, and how the tree is built: [Installing Packages](/nodejs/installing-packages).