Modules and npm

Lock dependency versions

Understand why package-lock.json records a complete dependency tree and when npm ci is useful.

package.json declares acceptable version ranges. package-lock.json records the exact dependency tree npm resolved.

Say a direct dependency uses ^4.18.0. That range can accept newer compatible releases. That package has its own dependencies with their own ranges. Without a lockfile, an install next month can pull a different complete tree even though your package.json did not change.

The lockfile records direct and transitive versions plus integrity hashes. Commit it for an application unless the project has a documented reason not to. Review lockfile changes together with the dependency command that caused them.

Use npm install when you intentionally change dependencies. It resolves a valid tree, updates node_modules, and updates the lockfile when needed.

Use a clean install in CI and deployment:

npm ci

npm ci requires an existing lockfile. It removes the existing node_modules directory, installs the locked tree, and never rewrites package.json or the lockfile. If the manifests disagree, it fails instead of silently fixing them. That failure protects the tree you reviewed.

Flags that change dependency resolution must stay consistent. If the lockfile was created with legacy-peer-deps, commit the matching project config rather than hoping one developer remembers the flag.

A lockfile improves reproducibility. It does not make dependencies secure or make every operating system identical. Native and optional packages can still differ by platform, and known vulnerabilities remain a separate problem. Pin the Node and npm versions in CI when exact tooling behavior matters.

Try this in a disposable project: run npm ci, change one dependency range in package.json without updating the lockfile, and confirm the next clean install fails. Then use the normal dependency workflow to restore agreement.

Lesson completed