npm 07 ๐ package-lock.json vs package.json
The two files serve opposite purposes in the same dependency system. package.json is the manifest: it declares the project’s metadata, scripts, and the ranges of acceptable dependency versions. package-lock.json is the resolution: it records the exact version of every package that was actually installed, including transitive dependencies that package.json never mentions. The distinction matters because npm install reads both files, and its behavior changes depending on whether the lock file exists and whether it agrees with the manifest .
The lock file was introduced in npm v5 to solve a specific problem: two developers running npm install on the same package.json could get different node_modules trees. If package.json specified ^1.4.0, one developer might install 1.4.0 on Monday and another might install 1.5.2 on Friday, after a new patch release was published. The code was nominally compatible, but the trees differed. package-lock.json pins every version to what was installed the first time, so subsequent installs reproduce the same tree regardless of what the registry has published since .
This chapter covers the manifest/resolution distinction, the fields each file contains, the npm install versus npm ci decision that depends on the lock file, the merge-conflict resolution that keeps the lock file honest, and the scenarios where the two files disagree and which one wins. It assumes familiarity with SemVer ranges and the npm install command from the preceding chapter.
Key point: package.json describes intent; package-lock.json records reality. The manifest says “I accept any version in this range”; the lock file says “this is the exact tree I installed.” Commit both. Use npm install when you want to update the lock file; use npm ci when you want to enforce it.
Why the two files exist separately
A range is not a version. package.json accepts SemVer ranges: ^1.2.3, ~1.2.3, >=1.0.0. A range describes a set of acceptable versions, not a single version. When npm install runs without a lock file, it resolves each range to the latest version that satisfies it, and then resolves the transitive dependencies of those packages. The result depends on the state of the registry at the moment of installation .
Transitive dependencies are invisible to the manifest. package.json lists direct dependencies only. A package that depends on lodash and axios does not list them in the consumer’s package.json. Without a lock file, npm resolves the transitive tree fresh on every install. A new patch release of a transitive dependency changes the tree, even though the consumer’s manifest never changed. The lock file records the transitive tree so it can be reproduced exactly .
The lock file is not a replacement for the manifest. package-lock.json contains only dependency resolution data. It does not contain the project name, version, description, scripts, or the dependencies ranges that define what the project needs. A project without package.json cannot be installed or published, because npm does not know what the project is or what ranges to resolve if the lock file is deleted .
The trade-off. Committing the lock file produces large diffs when dependencies change, and merge conflicts in the lock file are common on active branches. The alternativeโnot committing itโproduces non-reproducible installs, where the same manifest yields different trees on different machines and at different times. The npm documentation is explicit: the lock file “is intended to be committed into source repositories” .
a. package.json: the manifest
package.json is created by npm init and edited by hand. It contains the project’s identity (name, version, description, license), its entry point (main), its scripts (scripts), and its dependency ranges (dependencies, devDependencies, peerDependencies, optionalDependencies). It is the only file that npm needs to resolve a tree when no lock file exists .
{
"name": "my-app",
"version": "1.0.0",
"description": "Example project",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.18.0",
"lodash": "~4.17.21"
},
"devDependencies": {
"jest": "^29.0.0"
}
}
The dependencies field uses SemVer ranges. ^4.18.0 accepts >=4.18.0 <5.0.0; ~4.17.21 accepts >=4.17.21 <4.18.0. The choice of range determines how much freedom npm has when resolving without a lock file. A caret range accepts minor and patch updates; a tilde range accepts only patch updates; an exact version accepts only that version .
The manifest is also the source of the scripts that npm runs: npm start, npm test, npm run build. These are project commands, not dependency resolution, and they have no equivalent in the lock file.
b. package-lock.json: the resolution
package-lock.json is generated automatically whenever npm modifies node_modules or package.json. It describes the exact tree that was generated, including every transitive dependency, the resolved URL for each package, and an integrity hash that verifies the package was not modified .
{
"name": "my-app",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-...",
"dependencies": {
"accepts": "~1.3.8"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-..."
}
}
}
The lockfileVersion field indicates the format version. npm v7 and v8 use version 2; npm v9 and above use version 3. Older versions are automatically updated when npm detects them during installation . The resolved field points to the tarball URL, and integrity contains a Subresource Integrity hash that npm verifies before installing. If the hash does not match, the install fails, which prevents a compromised or mutated package from being installed silently .
The lock file also records devDependencies and optionalDependencies in the tree. When npm ci --omit=dev runs, it installs only the production dependencies from the lock file, skipping the development tree .
c. npm install versus npm ci
The two commands read the same files but behave differently. npm install is the general-purpose command: it resolves ranges, updates the lock file, and installs missing dependencies. npm ci is the CI-specific command: it requires an existing lock file, refuses to update it, deletes node_modules before installing, and exits with an error if the lock file and manifest disagree .
| Behavior | npm install | npm ci |
|---|---|---|
| Requires lock file | No | Yes |
| Updates lock file | Yes | No |
Deletes node_modules first | No | Yes |
| Fails on manifest/lock mismatch | No | Yes |
| Installs individual packages | Yes | No |
The npm ci behavior is the correct one for CI and deployment. It guarantees that the installed tree matches the committed lock file exactly, and it fails loudly if the manifest was changed without regenerating the lock file. The npm install behavior is correct for development, where adding a dependency should update the lock file .
A subtlety: if the lock file was created with flags that affect the dependency tree, such as --legacy-peer-deps, npm ci must be run with the same flags, or it may produce a different tree or fail. The fix is to set the flag in .npmrc and commit it, so both commands read the same configuration .
Complete Example Session
// ============================================
// PART 1: package.json โ THE MANIFEST
// ============================================
{
"name": "my-app",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.0",
"lodash": "~4.17.21"
}
}
// ============================================
// PART 2: package-lock.json โ THE RESOLUTION
// ============================================
{
"name": "my-app",
"lockfileVersion": 3,
"packages": {
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-..."
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-..."
}
}
}
// ============================================
// PART 3: WHAT EACH FILE CONTRIBUTES
// ============================================
// package.json:
// - Project name, version, description.
// - Scripts (start, test, build).
// - Dependency RANGES.
// - Entry point (main).
//
// package-lock.json:
// - EXACT versions of every package.
// - Transitive dependency tree.
// - Resolved URLs.
// - Integrity hashes.
// ============================================
// PART 4: npm install (DEVELOPMENT)
// ============================================
// Reads package.json.
// Resolves ranges to latest satisfying versions.
// Updates package-lock.json with the resolution.
// Installs node_modules.
// ============================================
// PART 5: npm ci (CI / DEPLOYMENT)
// ============================================
// Requires package-lock.json.
// Deletes node_modules.
// Installs exactly what the lock file specifies.
// Fails if package.json and lock file disagree.
// ============================================
// PART 6: WHEN THEY DISAGREE
// ============================================
// package.json: "express": "^4.19.0"
// package-lock.json: express 4.18.2
//
// npm install โ updates lock to 4.19.x.
// npm ci โ fails with an error.
// This is intentional: the lock file is stale.
// ============================================
// PART 7: MERGE CONFLICTS
// ============================================
// Lock file conflicts are common.
// Resolution:
// 1. Accept either version of the lock file.
// 2. Run npm install.
// 3. Commit the regenerated lock file.
// Never hand-edit the lock file.
// ============================================
// PART 8: WHAT TO COMMIT
// ============================================
// Commit: package.json, package-lock.json.
// Do not commit: node_modules/.
// The lock file makes node_modules reproducible.
// ============================================
// PART 9: npm-shrinkwrap.json
// ============================================
// Same format as package-lock.json.
// But publishable to the registry.
// If both exist, shrinkwrap wins.
// Use only for CLI tools distributed via npm.
// ============================================
// PART 10: THE FLOW
// ============================================
// Developer adds dependency:
// npm install axios โ updates both files.
// Commit both.
//
// CI runs:
// npm ci โ installs exact tree from lock.
// Fails if manifest and lock disagree.
//
// Deployment:
// npm ci --omit=dev โ production tree only.
Quick Reference
File Roles
| File | Role | Contains | Generated By |
|---|---|---|---|
package.json | Manifest | Metadata, scripts, ranges | npm init |
package-lock.json | Resolution | Exact versions, transitive tree | npm install |
npm install vs npm ci
| Behavior | npm install | npm ci |
|---|---|---|
| Requires lock file | No | Yes |
| Updates lock file | Yes | No |
Deletes node_modules | No | Yes |
| Fails on mismatch | No | Yes |
Lock File Fields
| Field | Purpose |
|---|---|
lockfileVersion | Format version (3 for npm 9+) |
resolved | Tarball URL |
integrity | SRI hash for verification |
packages | Tree of all installed packages |
What to Commit
| Item | Commit? |
|---|---|
package.json | Yes |
package-lock.json | Yes |
node_modules/ | No |
.npmrc (with flags) | Yes |
Best Practices
โ Do This:
// Commit both files. // โ
git add package.json package-lock.json
// Use npm ci in CI. // โ
npm ci --omit=dev
// Regenerate lock after resolving conflicts. // โ
npm install && git add package-lock.json
// Set flags in .npmrc for npm ci. // โ
legacy-peer-deps=true
โ Don’t Do This:
// Commit node_modules instead of lock file. // โ
git add node_modules/
// Hand-edit package-lock.json. // โ
// Regenerate with npm install.
// Run npm install in CI when lock file exists. // โ
// Use npm ci for reproducibility.
// Ignore lock file diffs in pull requests. // โ
// They show dependency changes that may be unexpected.
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Different trees on different machines | Lock file not committed | Commit package-lock.json |
| CI installs different versions | npm install instead of npm ci | Use npm ci |
| Lock file stale after manifest edit | Manual package.json edit | Run npm install to regenerate |
| Merge conflict in lock file | Multiple branches changed deps | Accept one, run npm install |
npm ci fails on peer deps | Flags differ from lock creation | Set flags in .npmrc |
Real-World Examples
1. Manifest Dependency Range
"dependencies": { "express": "^4.18.0" }
2. Lock File Exact Version
"node_modules/express": { "version": "4.18.2" }
3. Integrity Hash
"integrity": "sha512-..."
4. npm ci in CI
npm ci --omit=dev
5. Regenerate Lock File
npm install
6. Lock File Version
"lockfileVersion": 3
7. Resolved URL
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz"
8. Merge Conflict Resolution
git checkout --theirs package-lock.json
npm install
git add package-lock.json
9. .npmrc for Flags
legacy-peer-deps=true
10. npm-shrinkwrap.json
// Same format, publishable. Takes precedence.
Visual
Manifest vs Resolution
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ package.json package-lock.json โ
โ (INTENT) (REALITY) โ
โ โ
โ "express": "^4.18.0" express@4.18.2 โ
โ โ โ โ
โ โ range โ exact โ
โ โผ โผ โ
โ "Any 4.x.x" "This specific one" โ
โ โ
โ Transitive deps: Transitive deps: โ
โ not listed every one recorded โ
โ โ
โ npm install reads both. โ
โ Lock file wins when it exists. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Two Commands
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ npm install npm ci โ
โ โ
โ Development CI / Deployment โ
โ โ
โ Resolves ranges Requires lock file โ
โ Updates lock Never updates lock โ
โ Adds packages Clean install only โ
โ Flexible Strict โ
โ โ
โ Use when: Use when: โ
โ - Adding deps - Running tests โ
โ - Updating deps - Building artifacts โ
โ - Regenerating lock - Deploying โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
What Gets Committed
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ REPOSITORY โ
โ โ
โ package.json โ
commit โ
โ package-lock.json โ
commit โ
โ node_modules/ โ gitignore โ
โ .npmrc โ
commit (if flags) โ
โ โ
โ The lock file replaces node_modules โ
โ as the reproducible artifact. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
When They Disagree
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MANIFEST EDITED, LOCK STALE โ
โ โ
โ package.json: express ^4.19.0 โ
โ lock file: express 4.18.2 โ
โ โ
โ npm install: โ
โ Resolves ^4.19.0 โ 4.19.x. โ
โ Updates lock. โ
โ Installs 4.19.x. โ
โ โ
โ npm ci: โ
โ Detects mismatch. โ
โ Exits with error. โ
โ "npm ci can only install with an โ
โ existing package-lock.json..." โ
โ โ
โ This is the safety mechanism. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
package.json | Manifest: metadata, scripts, dependency ranges |
package-lock.json | Resolution: exact versions, transitive tree |
| Lock file generated by | npm install |
| Lock file committed | Yes, always |
node_modules committed | No |
npm install | Updates lock file, flexible |
npm ci | Requires lock, fails on mismatch, clean install |
lockfileVersion | 3 for npm 9+ |
integrity | SRI hash, prevents tampering |
npm-shrinkwrap.json | Publishable lock file, takes precedence |
Key takeaways:
package.jsonis intent;package-lock.jsonis reality. The manifest declares acceptable ranges and project metadata. The lock file records the exact tree that was installed, including transitive dependencies the manifest never mentions .- The lock file is not optional. Without it, two
npm installruns on the same manifest can produce different trees, because the registry changes between runs. The lock file makes installs reproducible across machines and time . npm installupdates the lock;npm cienforces it. Usenpm installwhen adding or updating dependencies. Usenpm ciin CI and deployment, where a mismatch between manifest and lock should fail the build rather than silently resolve .- Commit both files, never
node_modules. The lock file is the reproducible artifact.node_modulesis derived from it and should be gitignored. - Merge conflicts in the lock file are resolved by regeneration. Accept either side, run
npm install, and commit the result. Hand-editing the lock file risks corrupting the integrity hashes. npm-shrinkwrap.jsonis the publishable variant. Same format, but it can be published to the registry and takes precedence overpackage-lock.json. Use it only for CLI tools distributed via npm .
Remember: The two files are a division of labor. package.json says what the project is and what dependency ranges it accepts. package-lock.json says exactly what was installed to satisfy those ranges, down to the transitive tree and the integrity hash of every tarball. The manifest is for humans and for resolution; the lock file is for reproducibility. npm install reads the manifest, resolves ranges, and updates the lock. npm ci reads the lock, refuses to update it, and fails if the manifest has drifted. Commit both, run npm ci in CI, and regenerate the lock after resolving conflicts. The lock file is not a cache; it is the record of the tree that worked.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!