HomeProjectsBlogContact

Managing npm Dependencies Without the Headaches

Semantic versioning, lockfiles, npm ci, safe updates, security audits and choosing packages wisely. The habits that keep a JavaScript project installable, secure and up to date for years.

Managing npm Dependencies Without the Headaches

A typical JavaScript project depends on hundreds of packages once you count dependencies of dependencies. That's a huge productivity win, until an install breaks, a security alert appears, or a package you rely on gets abandoned. Good dependency habits keep that power without the pain.

Understand semantic versioning

Most packages follow semver: MAJOR.MINOR.PATCH.

  • Patch (2.4.1 → 2.4.2): bug fixes, nothing should break.
  • Minor (2.4.2 → 2.5.0): new features, backwards compatible.
  • Major (2.5.0 → 3.0.0): breaking changes. Read the migration guide.

In package.json, version ranges control what's allowed:

Range Allows
"^2.4.1" Any 2.x.x at or above 2.4.1 (the default)
"~2.4.1" Any 2.4.x at or above 2.4.1
"2.4.1" Exactly 2.4.1

Semver is a promise, not a guarantee. Minor releases occasionally break things, which is why the lockfile matters.

Commit your lockfile

package-lock.json (or pnpm-lock.yaml, yarn.lock, bun.lock) records the exact version of every package installed, including nested ones. Commit it. Without it, two developers running npm install a week apart can get different code.

Use npm ci in CI and production

npm ci

npm ci installs exactly what the lockfile says, fails if package.json and the lockfile disagree, and starts from a clean node_modules. It's faster and reproducible: exactly what you want in CI and deployments. (GitHub Actions setup.)

Use npm install when you're intentionally adding or updating packages.

dependencies vs devDependencies

  • dependencies: needed when the app runs (a web framework, a database client).
  • devDependencies: needed only for development and building (test runners, linters, type definitions, bundlers).
npm install express
npm install -D vitest eslint

Production installs can skip dev dependencies (npm ci --omit=dev), keeping images smaller. (Docker basics.)

Update regularly, in small steps

Dependencies left alone for two years turn into a painful, risky upgrade project. Small, frequent updates are much easier:

npm outdated        # what's behind?
npm update          # update within your allowed ranges

For major versions, update one package at a time, read its changelog, run your tests, and commit.

Automate the boring part with Dependabot or Renovate. They open pull requests for updates, your CI runs the tests, and you merge the green ones. Group minor and patch updates to reduce noise.

Take security alerts seriously, but calmly

npm audit

npm audit reports known vulnerabilities in your dependency tree. Not every alert is urgent: a vulnerability in a dev-only tool that never touches user input is lower risk than one in your web server. Prioritise:

  1. Vulnerabilities in runtime dependencies that handle untrusted input
  2. High and critical severity with a fix available
  3. Everything else, during regular update cycles

npm audit fix applies compatible fixes; be careful with --force, which can install breaking major versions. (Security checklist.)

Choose packages carefully

Every dependency is code you didn't write but now ship. Before adding one, check:

  • Is it maintained? Recent releases, responsive issues.
  • Is it widely used? Popular packages get bugs found and fixed faster.
  • How big is it? Check its install size and what it pulls in.
  • Do I actually need it? A tiny utility you could write in five lines may not be worth a dependency.
  • Is the name exactly right? Typosquatted packages with near-identical names are a real attack. AI assistants can also suggest packages that don't exist, and attackers have registered such names. (Using AI assistants safely.)

Watch out for install scripts

Packages can run scripts during installation, which is a common route for malicious packages. Review unfamiliar packages, consider tools or settings that disable or restrict install scripts in CI, and keep your lockfile under review in pull requests.

Peer dependencies

Plugins often declare peer dependencies: "I work with React 18 or 19, but you install React." Warnings about unmet peer dependencies usually mean version mismatches between a plugin and its host library. Read them rather than reflexively forcing installs.

Housekeeping

  • Remove unused dependencies (tools like depcheck or knip find them).
  • Pin your Node.js version with an engines field or a .nvmrc file, so everyone uses the same runtime.
  • Pick one package manager per project and stick to it; mixing lockfiles causes confusion.

FAQ

Should I commit package-lock.json?

Yes. The lockfile guarantees everyone, including CI and production, installs exactly the same dependency versions.

What is the difference between npm install and npm ci?

npm install resolves and may update dependencies within allowed ranges and updates the lockfile. npm ci installs exactly what the lockfile specifies from a clean slate, making it ideal for CI and deployments.

How often should I update npm packages?

Regularly and in small batches, ideally weekly or monthly with automated pull requests from Dependabot or Renovate, plus prompt updates for serious security issues.

Keep reading

More from the workbench

All articles →