package-lock.json vs package.json
Two files, one intent file and one lockfile. Understanding the difference between them is the foundation of every reproducible Node.js install.
Every Node.js project ships two dependency files. One you edit by hand; the other was designed never to be touched. Confusing their roles leads to broken builds, unexpected upgrades, and merge conflicts that take longer to resolve than the feature they arrived with.
Intent vs lock: the core distinction
package.json is the intent file. It declares the packages your project depends on and the semver ranges you accept. A range like "^4.2.0" says "any 4.x release at or above 4.2.0 is acceptable." The intent is deliberately loose so you receive patch and minor fixes without manually bumping every entry.
package-lock.json (or yarn.lock, orpnpm-lock.yaml) is the lockfile. It records the exact resolved version, the full dependency tree, integrity hashes, and installation metadata for every package. When the lockfile is present, the package manager installs from the lockfile and ignores the ranges inpackage.json — unless you explicitly ask it to re-resolve.
The two files work together. The manifest says what you want; the lockfile says what you got and what everyone else should get too.
The early days: no lockfile at all
Before npm 5 (released May 2017), npm install resolved the dependency tree fresh every time. Two developers running npm install on the same package.json on different days could receive different versions of transitive dependencies — a new patch release of a sub-dependency might change behavior without anyone noticing until it reached production.
npm offered npm-shrinkwrap.json as an opt-in lockfile as early as npm 2 (2014), but it was opt-in and rarely used. Shrinkwrap was a user-facing contract you could publish to the registry, which made it different from the modern lockfile. The ecosystem mostly got by on vague hope until npm 5 made locking the default.
npm 5 and the v1 lockfile
npm 5 introduced package-lock.json as a first-class file. Every install automatically generated or updated it. The v1 lockfile format was a single JSON object mapping package names to their resolved version, resolved URL, integrity hash (SHA-512 in base64), and dependencies. It was human-readable but deeply nested — a project with a hundred direct dependencies could produce a lockfile tens of thousands of lines long.
The v1 format had quirks. It could change formatting or reorder entries after a seemingly identical install, creating noisy diffs. It also did not handle optional peer dependency trees cleanly, and certain edge cases around git dependencies produced inconsistent entries.
Lockfile v2 and v3: stability and determinism
npm 7 (October 2020) shipped lockfile v2. The format switched to a flat list of packages keyed by their node_modules path, with apackages top-level array alongside a backward-compatibledependencies section. This made the lockfile more deterministic and diff-friendly. Entries stopped moving around when unrelated packages changed.
npm 9 (January 2023) introduced lockfile v3. It removed the legacydependencies section, keeping only the flatpackages map, and added an explicitlockfileVersion integer at the top. The format is leaner and makes the same dependency tree easier to reproduce and cache in CI.
Integrity metadata
Every entry in a lockfile includes an integrity field — a Subresource Integrity (SRI) string using SHA-512. When npm downloads a tarball, it verifies the hash against the lockfile entry before extracting it. If the registry returns a different file than what was recorded, the install fails. This helps detect an unexpected artifact or a man-in-the-middle change, but it cannot judge whether a package was malicious when it was first published.
The integrity hash is computed from the tarball content, not the package metadata. Two different tarballs of the same package can have different hashes, even when their source code is identical. A lockfile regeneration can therefore change an integrity field when the resolved artifact changes; treat that as a reason to inspect the diff rather than as proof of an attack.
Other package managers
Yarn uses yarn.lock, a custom text format (not JSON) that lists every resolved version with its resolved URL and integrity hash in a single flat list. Yarn Classic (v1) was the first major package manager to make lockfiles the default, beating npm by over a year (released 2016). Yarn Berry (v2+) keeps the same format with minor extensions for the.pnp.cjs resolution mode.
pnpm uses pnpm-lock.yaml, a YAML format that records the dependency graph as a list of importers (your workspace packages) and a flat package snapshot. pnpm's lockfile is the most compact of the three because YAML's indentation avoids JSON's curly-brace overhead, and pnpm aggressively deduplicates identical package versions into a single entry.
Practical rules for working with lockfiles
Commit the lockfile. This is settled. The lockfile is not a build artifact — it is the source of truth for what runs in CI, on your teammates' machines, and in production. Omitting it trades reproducibility for a slightly smaller initial clone.
Never hand-edit the lockfile. Lockfiles are machine-written and not designed for human editing. A typo in a version string or a missing integrity hash will fail the install or, worse, silently fall back to a different resolution. If you need to change a dependency, editpackage.json and run the package manager's install command.
Regenerate with the same package manager every time.Switching between npm, Yarn, and pnpm without removing the old lockfile produces orphan entries and conflicting resolution strategies. Delete the lockfile and node_modules when migrating, then regenerate from scratch.
Review lockfile diffs during code review. A lockfile change without a corresponding package.json change means a transitive dependency resolved differently — worth understanding before approving. Large lockfile diffs from a version bump should only affect the bumped package and its subtree.
Resolve lockfile conflicts by regenerating. Git merge conflicts in a lockfile are common when two branches add different dependencies. The cleanest fix is to accept either side, then run the package manager install command to produce a consistent merged lockfile. Do not try to edit the conflict markers manually.
Pin the package manager version in CI. UsepackageManager in package.json (Corepack) or an explicit version in your CI configuration. Different lockfile versions are produced by different npm versions, and a CI runner with a newer npm may upgrade the lockfile format automatically.
Summary
package.json declares intent with semver ranges.package-lock.json pins the exact tree. Both are essential, both belong in version control, and neither should be hand-edited. Understanding their distinct roles prevents a whole class of "works on my machine" bugs and keeps dependency management predictable across environments.
Sources and further reading
- npm Docs: package-lock.json
- npm Docs: package.json
- npm 7.0.0 release notes — the release that introduced lockfile v2 and npm's modern peer resolver.
- Yarn Classic: yarn.lock documentation
- pnpm: Lockfile documentation
- Node.js Blog: Managing dependencies with npm-shrinkwrap (2014)
- npm 5.0.0 release notes
- npm/package-json: The package.json specification
- W3C: Subresource Integrity specification