package-lock.json is the unsung hero of reproducible Node.js builds, ensuring your project installs the exact same dependency versions every single time.

Let’s watch it in action. Imagine you have a package.json like this:

{
  "name": "my-app",
  "version": "1.0.0",
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

When you run npm install for the first time, npm does more than just read package.json. It resolves the exact versions of lodash and all of its dependencies that satisfy ^4.17.21 (which means "latest 4.x.x"). It then writes these exact versions, along with their entire dependency tree, into package-lock.json:

{
  "name": "my-app",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "my-app",
      "version": "1.0.0",
      "dependencies": {
        "lodash": "^4.17.21"
      }
    },
    "node_modules/lodash": {
      "version": "4.17.21",
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
      "integrity": "sha512-v2kDEe5myemj0oFf4jK9p9o0530G2h8/u/W9fCgN2fC3T5LwzJ0o1M+gY5Z2K+4H5/Q==",
      "engines": {
        "node": ">=4.0.0"
      }
    }
  }
}

Notice the resolved and integrity fields. resolved points to the exact tarball URL from the npm registry, and integrity is a cryptographic hash of that tarball. This is crucial.

Now, if you or a colleague run npm install on a different machine, or even on the same machine weeks later, npm will ignore package.json’s version ranges for lodash (like ^4.17.21). Instead, it will read package-lock.json and install exactly version 4.17.21 of lodash, using the specified tarball and verifying its integrity. This eliminates "it works on my machine" problems stemming from dependency version drift.

The problem package-lock.json solves is the inherent ambiguity in version ranges. Without it, npm install might fetch 4.18.0 today, and 4.19.0 tomorrow, even though package.json only specified ^4.17.21. This subtle shift can introduce breaking changes, especially in larger projects with complex dependency trees. package-lock.json creates a fixed snapshot of your dependencies.

Think of package.json as your desired state (e.g., "I want lodash version 4 or higher") and package-lock.json as the actual, concrete state that achieved that desired state at a specific point in time (e.g., "At 10:05 AM on Tuesday, version 4.17.21 was the correct choice, and here’s its exact download location and checksum").

When you update a dependency using npm update lodash or npm install lodash@latest, npm will first try to satisfy the new version range in package.json. If it finds a newer version that fits, it will update the package.json and regenerate the relevant parts of package-lock.json to reflect the new exact version, its resolved URL, and its integrity hash. If you were to manually edit package.json to lodash: "5.0.0" and run npm install, npm would try to find and install 5.0.0, updating package-lock.json accordingly.

The packages field in package-lock.json is a flat representation of your entire dependency tree, not just your direct dependencies. This means even transitive dependencies (dependencies of your dependencies) are locked down. For example, if lodash depends on a specific version of some-other-lib, that version of some-other-lib will also have its resolved URL and integrity hash recorded in package-lock.json. This is why package-lock.json can grow quite large in complex projects.

One of the most misunderstood aspects is how npm ci (clean install) differs from npm install. While npm install can update package-lock.json if package.json is modified or if npm update is run, npm ci is strictly for production or CI environments. It only installs dependencies based on package-lock.json and will fail if package.json and package-lock.json are out of sync. It also deletes the node_modules directory first, ensuring a completely clean slate.

When you’re working with multiple developers or deploying to different environments, committing package-lock.json alongside package.json is non-negotiable. It’s the contract that guarantees everyone is working with the identical set of dependencies, preventing subtle bugs and deployment surprises.

If you ever encounter a situation where npm install seems to be ignoring your package-lock.json entirely, the most common culprit is actually having npm version 5.0.0 or older installed. These early versions had bugs where package-lock.json might not be respected or generated correctly. Upgrading npm to a more recent stable version (e.g., npm install -g npm@latest) usually resolves these issues.

The next hurdle you’ll face is understanding how to manage security vulnerabilities within your locked dependency versions.

Want structured learning?

Take the full Npm course →