Versions & the Lockfile

Reading a version number

npm packages follow semantic versioning ("semver"): three numbers separated by dots, each with a meaning. Take 6.4.2:

  • 6 = MAJOR — bumped for breaking changes that may require you to update your code.
  • 4 = MINOR — bumped for new features that are backward-compatible (your code keeps working).
  • 2 = PATCH — bumped for backward-compatible bug fixes only.

So upgrading 1.4.2 → 1.4.3 is a safe bug fix, 1.4.2 → 1.5.0 adds features safely, and 1.4.2 → 2.0.0 is the one to read the release notes for.

The caret (^): a range, not a fixed version

In package.json you saw versions written like ^6.4.2. The caret turns a single version into a range. It means: "this version, or any newer one that doesn't change the MAJOR number". So ^6.4.2 permits 6.4.3, 6.5.0, up to (but not including) 7.0.0.

The logic: allow safe minor and patch updates automatically, but never silently jump to a new major that might break things. A couple of other symbols you'll meet:

  • ~6.4.2 (tilde) — stricter: only patch updates (6.4.x).
  • 6.4.2 (no symbol) — pinned: exactly that version, nothing else.

So which version do I actually get? The caret describes what's allowed. The exact version you install is recorded separately — in the lockfile. That's the key idea of this chapter.

Why package-lock.json exists

Here's the problem the lockfile solves. Say package.json allows ^6.4.2. You install today and get 6.4.2. A teammate installs next month, by which time 6.5.0 has been released — so they get a different version. Multiply that across hundreds of packages and "works on my machine" bugs are born.

package-lock.json fixes this. It's an automatically-generated file that records the exact version of every package actually installed — including all the transitive ones. Commit it to Git, and everyone who runs npm install gets a byte- for-byte identical node_modules.

  • Do commit package-lock.json to your repository.
  • Don't hand-edit it — npm manages it for you.
  • Think of it as: package.json states your intent (a range); the lockfile records the reality (an exact tree).

npm install vs npm ci

There are two ways to install, and the difference is all about the lockfile:

  • npm install — installs within the ranges in package.json and may update the lockfile if something newer is allowed. This is what you use while developing.
  • npm ci — "clean install": it deletes node_modules and installs the lockfile exactly, never changing it. It's faster and perfectly reproducible, which is why automated deploys and CI systems use it.

Everyday install vs reproducible install

Terminal
$ npm install   # dev: resolve ranges, may update the lockfile
$ npm ci        # CI/deploy: install the lockfile exactly
In VS Code
  1. Both run in the integrated terminal
  2. Use npm install day to day
  3. Reach for npm ci when you want a guaranteed-clean, lockfile-exact install

npm ci requires an existing package-lock.json and will error if it disagrees with package.json — that mismatch is itself a useful warning.

Up next: npx — running a package's command once without installing it at all.

FAQ

Frequently Asked Questions

What does the caret in "^4.3.0" mean?
The ^ means "compatible with 4.3.0" — npm may install any 4.x.x up to but not including 5.0.0. It allows safe minor and patch updates while blocking breaking major changes.
What is package-lock.json?
A machine-generated file recording the exact version of every package (including transitive ones) that was installed. Commit it so everyone — and your deploy — gets an identical node_modules.
What is the difference between npm install and npm ci?
npm install may update the lockfile to satisfy package.json. npm ci wipes node_modules and installs the lockfile exactly — faster and reproducible, which is why CI/CD and deploys use it.
What is semantic versioning?
A MAJOR.MINOR.PATCH numbering scheme: bump PATCH for bug fixes, MINOR for backward-compatible features, and MAJOR for breaking changes — e.g. 1.4.2 → 2.0.0.