Skip to content
All posts
Tutorial7 min read

package.json vs package-lock.json – What's the Difference?

J
Jamith Nimantha
April 3, 2026
Colorful source code on a dark monitor
Photo by Unsplash
On this page

package.json says what you want. package-lock.json records what you got.

You write "express": "^4.18.2", which is a range covering every 4.x release. The lockfile pins the one exact version that was installed, plus every transitive dependency underneath it, so the next person who installs gets a byte-identical tree instead of whatever is newest that day.

Both belong in git. The rest of this explains why, and what goes wrong when they disagree.

What is package.json?

package.json is the project manifest file. It describes your project and lists the dependencies your application needs.

It typically includes:

  • Project name and version
  • Scripts (npm start, npm build, etc.)
  • Dependencies and dev dependencies
  • Metadata (author, license, etc.)

Example

json
{
  "name": "my-app",
  "version": "1.0.0",
  "dependencies": {
    "express": "^4.18.2"
  }
}

Key Point

When you write "express": "^4.18.2", the ^ means npm can install any compatible version, such as 4.18.2, 4.19.0, or any 4.x.x release.

This means installs may differ over time, which can cause problems.

Reading a version range

The character in front of the version decides how much drift you are allowing. This is the whole reason the lockfile has to exist.

RangeMatchesAllows
4.18.2exactly 4.18.2nothing
~4.18.24.18.xpatch releases
^4.18.24.x.xminor and patch
^0.4.20.4.xpatch only, because 0.x treats minor as breaking
* or latestanythingeverything, including major versions

^ is the npm default, so unless you have deliberately changed it, every dependency you install is a range rather than a version. That is the correct default for a library and a liability for an application, and the lockfile is what makes it safe.

[!NOTE] The ^0.x rule catches people out. Under semver, a package below 1.0 has no stability guarantee, so npm narrows ^ to patch-only there. A jump from 0.4.2 to 0.5.0 is treated as breaking and will not be installed automatically.

What is package-lock.json?

package-lock.json is automatically generated by npm. It locks the exact versions of every installed dependency, including sub-dependencies.

It ensures:

  • Same dependency tree across all environments
  • Same versions for everyone on the team
  • Reproducible installs across machines

Example (simplified)

json
{
  "dependencies": {
    "express": {
      "version": "4.18.2",
      "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
      "integrity": "sha512-..."
    }
  }
}

This file guarantees that everyone installs the exact same version, even if package.json allows flexibility.

Main Differences

Featurepackage.jsonpackage-lock.json
PurposeDefines dependenciesLocks exact versions
Created byDevelopernpm automatically
Should you edit?YesNo
Version flexibilityYesNo
Included in Git?YesYes
Controls sub-dependenciesNoYes

Why Both Files Are Needed

Think of it like this:

  • package.jsonWhat you want
  • package-lock.jsonWhat you actually got

Example scenario:

  1. You install today → version 1.2.3
  2. Your teammate installs tomorrow → version 1.2.5
  3. App breaks due to subtle differences

With package-lock.json, everyone installs exactly 1.2.3. No surprises.

When Does package-lock.json Update?

It updates when:

  • You run npm install
  • You add or remove packages
  • You run npm update

[!TIP] A lock file for a real project runs to thousands of lines, and a merge conflict in one is genuinely unpleasant to read. Dropping it into the JSON Parser collapses the tree so you can find the one dependency that actually changed.

Should You Commit package-lock.json?

Yes, always commit it to Git.

Benefits:

  • Reproducible builds
  • Faster installs (npm can skip resolution)
  • Prevents "works on my machine" issues

npm install vs npm ci

This is the practical payoff of having a lockfile, and the part most people never switch on.

npm installnpm ci
Readspackage.json, then the lockfilethe lockfile only
Can change the lockfileyesno, it errors instead
Needs an existing lockfilenoyes
node_modulesupdated in placedeleted and rebuilt
Use itwhile developingin CI and any reproducible build

npm install treats the lockfile as a suggestion. If package.json allows a newer version than the lockfile pins, it will happily resolve upward and rewrite the lockfile as a side effect, which is how a lockfile change sneaks into a pull request nobody meant to touch.

npm ci treats the lockfile as the source of truth. If the two files disagree it fails rather than guessing:

text
npm error code EUSAGE
npm error `npm ci` can only install packages when your package.json and
npm error package-lock.json or npm-shrinkwrap.json are in sync. Please update
npm error your lock file with `npm install` before continuing.
npm error
npm error Missing: is-odd@3.0.1 from lock file

That error is the feature. It means someone edited package.json without running an install, and the build stopping is better than the build silently producing a different tree from the one that was tested.

Use npm ci in CI, in Docker builds, and anywhere reproducibility matters. It is also faster, because it skips resolution entirely.

lockfileVersion, and why the diff exploded

The first line of the lockfile is a format version:

json
{ "name": "my-app", "lockfileVersion": 3 }
  • v1: npm 6. No transitive integrity data.
  • v2: npm 7 to 8. Backwards compatible with v1 readers, which is why it was so large: it carried both formats at once.
  • v3: npm 9 and later. Drops the v1 compatibility block, so the file is considerably smaller.

If a colleague's install produces a 20,000 line diff on a file you did not touch, this is usually why: someone is on a different major npm version and the format is being rewritten. Fix it by agreeing a version, ideally with an engines field and a .nvmrc.

When the two files disagree

The failure mode nobody warns you about. package.json is the file humans edit, and the lockfile is generated, so they drift:

  • Someone hand-edited package.json. The lockfile still pins the old version. npm install fixes it, npm ci fails loudly.
  • A merge brought in two different dependency sets. Do not hand-resolve the lockfile. Take either side, then run npm install to regenerate it from the merged package.json.
  • A transitive dependency published a bad release. Use overrides to force a version without waiting for the direct dependency to update:
json
{
  "overrides": {
    "semver": "7.5.4"
  }
}

To see what is actually installed rather than what was requested, npm ls <package> prints the resolved tree and shows you which parent pulled it in.

Quick Analogy

  • package.json = Shopping list
  • package-lock.json = Receipt with exact brands and quantities

Conclusion

Both files serve different but complementary purposes:

  • Use package.json to declare what dependencies you need
  • Use package-lock.json to guarantee everyone gets the same versions

Never delete package-lock.json unless you intentionally want to regenerate your entire dependency tree. If you think you need to, the fix is usually npm ci on a clean checkout instead.

One change worth making today: switch your CI job from npm install to npm ci. It is a one-word edit, it makes your builds reproducible, and it turns a silent dependency drift into a failed build with a clear message.

Tools in this post

Related Tool

JSON Parser & Formatter

Validate, format, and minify JSON data with error highlighting.

Try it free
J

Written by

Jamith Nimantha

Software developer. Builds the DebuggerMe tools and writes about the things he runs into shipping them.

Share this post

Back to all posts

Related Articles

All articles →