The package-lock.json guide

By

Learn how package-lock.json makes npm installs repeatable, how it differs from package.json, and when to use npm install or npm ci.

~~~

The package-lock.json file records the exact dependency tree installed by npm.

You normally do not edit it by hand. npm creates and updates it when an install changes your project.

The most important rule is simple: commit package-lock.json to Git. It lets your teammates, CI jobs, and deployments start from the same dependency tree you tested.

In this guide we’ll see what the lockfile contains, how npm uses it, and what to do when it changes.

package.json and package-lock.json solve different problems

package.json describes the dependencies your project accepts:

{
  "dependencies": {
    "fastify": "^5.6.0"
  }
}

The caret means npm can install compatible releases within the same major version. A future install might choose 5.7.0 or 5.8.1.

That range is useful. It tells npm which updates the project allows.

But an allowed range does not identify one exact dependency tree. Fastify also has dependencies, and those packages have dependencies of their own. Their allowed versions can change too.

package-lock.json records the result npm selected:

{
  "name": "notes-api",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "dependencies": {
        "fastify": "^5.6.0"
      }
    },
    "node_modules/fastify": {
      "version": "5.6.0",
      "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.6.0.tgz",
      "integrity": "sha512-..."
    }
  }
}

I shortened the integrity value here. A real lockfile contains the complete value and many more package entries.

Think about the two files this way:

You need both.

Why the lockfile matters

Without a lockfile, two valid installs can produce different trees.

Suppose you install a project on Monday. One of its indirect dependencies releases a compatible update on Tuesday. Your teammate installs the project on Wednesday.

Both installations satisfy package.json, but they contain different code.

That can produce the worst kind of bug: one that only happens on another machine.

The lockfile reduces this uncertainty. npm can reconstruct the recorded tree instead of resolving every compatible version again.

This gives you:

A lockfile does not make two computers perfectly identical. Native packages can differ by operating system or CPU. Optional dependencies can be platform-specific. Node.js and npm versions also affect behavior.

Pin those tools in CI when exact reproducibility matters.

What npm stores in package-lock.json

Modern npm lockfiles use a packages object. Each key represents the project root or an installed package location.

Common fields include:

You do not need to memorize the format. The official package-lock.json reference documents every field.

The useful skill is reading a diff. When a dependency update changes 200 lines, look for which direct package moved, which indirect packages appeared, and whether any resolved source changed.

lockfileVersion

The lockfileVersion field describes the lockfile format, not your application’s version.

Current npm versions use format version 3 for package-lock.json. Older npm releases produced earlier formats. npm can usually read older lockfiles and update them when needed.

This is why a large lockfile-only diff can appear after someone uses a different npm version. The dependency versions might be unchanged while the representation changes.

My advice is to use the same Node.js and npm versions across your team and CI. It prevents noisy rewrites and makes install behavior easier to explain.

Do not manually change lockfileVersion to make a diff smaller. Let the npm version chosen by the project produce the file.

The hidden lockfile inside node_modules

Recent npm versions can also create node_modules/.package-lock.json.

This hidden lockfile helps npm avoid repeatedly reading every installed package directory. It is an installation cache, not the project contract you review and commit.

Do not copy it to the repository root. Do not commit node_modules to preserve it. npm recreates the hidden file while managing the installed tree.

The root package-lock.json remains the source-controlled record. If node_modules looks inconsistent, remove the installed directory and run npm ci instead of trying to repair the hidden lockfile.

npm install and npm ci

npm install and npm ci both install dependencies, but they have different jobs.

Use npm install while developing and intentionally changing dependencies:

npm install fastify

npm updates package.json, resolves a valid dependency tree, installs it, and updates package-lock.json.

Running plain npm install also checks the relationship between the manifest and lockfile. It may update the lockfile when the manifest allows or requires a different tree.

Use npm ci when you want a frozen clean install:

npm ci

The command requires an existing lockfile. If package.json and the lockfile disagree, it fails instead of fixing them. It removes an existing node_modules directory and installs the complete locked project without rewriting either manifest.

That behavior makes npm ci the right default for CI and deployments. A stale lockfile becomes a clear error instead of a surprise change during the build.

One detail matters. If the lockfile was created with a tree-shaping option such as --legacy-peer-deps, npm ci needs the same option. Store project-wide settings in a committed .npmrc so every environment applies them.

The official npm ci documentation lists the exact differences.

Add, update, and remove dependencies

Let npm update both files together.

To add a runtime dependency:

npm install fastify

To add a development dependency:

npm install --save-dev eslint

To remove one:

npm uninstall fastify

To update packages within the ranges allowed by package.json:

npm update

After each intentional change, run the tests and review both diffs.

Do not run npm update only to make the lockfile look fresh. Dependency updates change code. Treat them like any other code change.

If you only need npm to recalculate the lockfile without installing packages, npm provides:

npm install --package-lock-only

This is useful in controlled maintenance work, but it is not a substitute for testing the installed result.

Should libraries commit a lockfile?

Applications should commit package-lock.json. The deployed application is the thing you want to reproduce.

Libraries can commit it too. It makes development and CI repeatable for the library maintainers.

There is one important boundary: npm does not publish package-lock.json with a package. People installing your library resolve its declared dependency ranges in their own project.

That means library tests should not only prove that yesterday’s locked tree works. You also need routine dependency updates so you notice when allowed ranges stop working.

npm-shrinkwrap.json is different. It has the same format but can be published with a package. That makes it appropriate for some command-line applications, but usually too restrictive for reusable libraries.

Workspaces use one root lockfile

An npm workspace normally uses the lockfile at the repository root.

That file records the packages in every workspace and how npm links them together. Do not create a separate lockfile inside each workspace unless the repository deliberately treats it as an independent project.

Run installation commands from the root:

npm install

Then commit the root package.json, workspace manifests, and root lockfile as one coherent change.

Resolve merge conflicts safely

Lockfiles are generated, but they still represent an important decision. Avoid choosing one side of a conflict without checking the manifests.

Start by resolving the package.json changes. Then let npm rebuild a tree that satisfies the resolved manifests:

npm install

Run your tests and inspect the resulting lockfile diff.

For a sensitive dependency change, a clean check is useful:

npm ci
npm test

Do not hand-merge hundreds of nested lockfile entries. It is easy to create a file that parses but does not describe the tree you intended.

What the integrity field does

The integrity field lets npm verify that downloaded package content matches the recorded digest.

This protects against corrupted downloads and content that does not match the lockfile. It does not prove that the package itself is trustworthy.

A malicious release can have a perfectly valid integrity value. A lockfile can also preserve a vulnerable version forever.

Keep reviewing package changes, run security checks, and update dependencies deliberately. Reproducibility and security support each other, but they are not the same thing.

Common questions

Should I add package-lock.json to .gitignore?

No. Commit it for applications and for the development environment of libraries.

Should I edit it by hand?

No. Change the dependency through npm and review the generated diff.

Can I delete it?

You can, but the next install resolves a new tree. That can update many indirect dependencies at once.

Delete and regenerate it only when you understand why the existing tree cannot be repaired. Test the result as a real dependency update.

Why did it change when package.json did not?

Someone may have used a different npm version, refreshed metadata, or run an update command. Inspect the diff and the command history before committing it.

Does package-lock.json replace node_modules?

No. The lockfile is a description. node_modules contains the installed files.

You normally commit the description and rebuild the installed directory with npm ci.

The workflow I use

For an application, I keep the workflow boring:

  1. Pin the Node.js version used by the project.
  2. Change dependencies with npm.
  3. Review package.json and package-lock.json together.
  4. Run the tests.
  5. Commit both files.
  6. Use npm ci in automation.

If npm ci fails, I fix the disagreement locally and commit the real result. I never make the deployment silently regenerate its own dependency tree.

The lockfile is not noise. It is the exact dependency decision behind the code you tested.

Tagged: Node.js · All topics
~~~

Related posts about node: