| |

npm 07 ๐ŸŒ package-lock.json vs package.json

โฑ๏ธ Estimated read: 12 min | |

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 .

Behaviornpm installnpm ci
Requires lock fileNoYes
Updates lock fileYesNo
Deletes node_modules firstNoYes
Fails on manifest/lock mismatchNoYes
Installs individual packagesYesNo

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

FileRoleContainsGenerated By
package.jsonManifestMetadata, scripts, rangesnpm init
package-lock.jsonResolutionExact versions, transitive treenpm install

npm install vs npm ci

Behaviornpm installnpm ci
Requires lock fileNoYes
Updates lock fileYesNo
Deletes node_modulesNoYes
Fails on mismatchNoYes

Lock File Fields

FieldPurpose
lockfileVersionFormat version (3 for npm 9+)
resolvedTarball URL
integritySRI hash for verification
packagesTree of all installed packages

What to Commit

ItemCommit?
package.jsonYes
package-lock.jsonYes
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

PitfallWhy It HappensFix
Different trees on different machinesLock file not committedCommit package-lock.json
CI installs different versionsnpm install instead of npm ciUse npm ci
Lock file stale after manifest editManual package.json editRun npm install to regenerate
Merge conflict in lock fileMultiple branches changed depsAccept one, run npm install
npm ci fails on peer depsFlags differ from lock creationSet 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

ItemValue
package.jsonManifest: metadata, scripts, dependency ranges
package-lock.jsonResolution: exact versions, transitive tree
Lock file generated bynpm install
Lock file committedYes, always
node_modules committedNo
npm installUpdates lock file, flexible
npm ciRequires lock, fails on mismatch, clean install
lockfileVersion3 for npm 9+
integritySRI hash, prevents tampering
npm-shrinkwrap.jsonPublishable lock file, takes precedence

Key takeaways:

  • package.json is intent; package-lock.json is 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 install runs 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 install updates the lock; npm ci enforces it. Use npm install when adding or updating dependencies. Use npm ci in 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_modules is 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.json is the publishable variant. Same format, but it can be published to the registry and takes precedence over package-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!