How to Keep npm Packages Up to Date: A Practical Plan

In a large front-end project, the node_modules folder can hold over a thousand packages, and new versions of some of them come out every day. If you update whenever someone remembers to, you’ll eventually be several majors behind, with a security advisory you can’t apply because the fix depends on three other upgrades. If you merge every update the moment it appears, you’ll spend your week on dependency pull requests and nothing else. The answer is a routine: let tools watch for new versions continuously, and have people make decisions on a fixed schedule. Other posts cover the general idea, such as this one from ButterCMS. In this post I propose a concrete schedule for npm packages and front-end assets, and the tool configuration that makes it work.

Let a bot watch, and let people decide

Dependabot (built into GitHub), Renovate and Snyk all check your dependencies and open pull requests when new versions come out. Snyk focuses on vulnerabilities, while Dependabot and Renovate handle routine updates as well. Out of the box they tend to open one pull request per package, which gets noisy fast. Configure them to group related updates, run on a schedule, and wait a few days before suggesting a brand-new release:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "monday"
    cooldown:
      default-days: 5
      semver-major-days: 30
    groups:
      minor-and-patch:
        update-types:
          - "minor"
          - "patch"
    open-pull-requests-limit: 10

This gives you one grouped pull request a week for all minor and patch updates, which replaces my old habit of merging updates into a dedicated branch and testing them there. Major versions still get their own pull requests. Dependabot security updates are separate from this schedule. They’re opened as soon as an advisory affects you, and the cooldown doesn’t apply to them.

Renovate has more options, including grouping by package family, automerging low-risk updates and a scheduled lockfile refresh:

{
  "extends": ["config:recommended"],
  "schedule": ["before 9am on monday"],
  "minimumReleaseAge": "3 days",
  "lockFileMaintenance": {
    "enabled": true,
    "schedule": ["before 9am on the first day of the month"]
  },
  "packageRules": [
    {
      "matchUpdateTypes": ["minor", "patch"],
      "groupName": "minor and patch updates"
    },
    {
      "matchDepTypes": ["devDependencies"],
      "matchUpdateTypes": ["patch"],
      "automerge": true
    }
  ]
}

Both tools have built-in groups or presets for monorepo packages, which have to move together. Examples are react with react-dom, all the @angular/* packages, and an ESLint config together with its plugins. Updating only half of a family like that is a common way to break a build.

A schedule that works

Continuously: security fixes

When the bot or npm audit reports a vulnerability that actually affects your app, deal with it in days, not at the next review. Check whether the vulnerable code is reachable before you drop everything. Many advisories are for dev-only tools, or for code paths you never call. If the fix is in a transitive dependency and the package that depends on it hasn’t released an update yet, you can force the fixed version with overrides in package.json (Yarn calls it resolutions, and pnpm uses pnpm.overrides):

{
  "overrides": {
    "semver": "^7.5.4"
  }
}

Write down why each override exists, and remove it once the parent package catches up.

Weekly: review the bot’s pull requests

Every Monday, go through the open update pull requests. For the grouped minor and patch updates, a green CI run plus a quick look at the changelogs is usually enough. For anything that touches your build tooling, test framework or UI library, read the release notes properly. If CI fails, don’t let the pull request sit there for weeks. Either fix the problem or close the pull request and tell the bot to ignore that version until you have time.

Every two weeks: look at the dependencies themselves

Every other Friday, look at the dependency list itself instead of the version numbers. Are there packages you no longer use? npx knip finds unused dependencies and exports. Are any packages losing maintainers or piling up open issues? Has the platform caught up with something you installed a package for, the way fetch, structuredClone and Array.prototype.at did? The e18e community keeps a list of lighter or native replacements for popular packages that’s worth checking. Every dependency you remove is one you’ll never have to update again.

Monthly: majors and the lockfile

Major versions can contain breaking changes, so I handle them once a month instead of whenever they show up. Read the migration guide, run the official codemods (ng update for Angular, react-codemod for React), upgrade one major per pull request, and test that pull request thoroughly before merging. Framework majors, like a new React or Next.js version or Angular’s twice-yearly releases, are bigger than a single pull request. Plan them as small projects with their own ticket.

This is also when the lockfile gets refreshed, either by Renovate’s lockFileMaintenance or by running npm update by hand. A refresh pulls in newer transitive dependencies within your existing version ranges, which the weekly pull requests for direct dependencies don’t cover. npm outdated, or npx npm-check-updates --interactive, shows what’s left to do.

Quarterly: review the process

Once a quarter, check whether the routine is working. Look at how many update pull requests were left open for more than two weeks, how far behind the latest version your main framework is, and whether an update broke production without any test catching it. That last question usually points to the real problem, which is test coverage. Check that your Node version is still supported too. Even-numbered Node releases become LTS each October and are supported for about 30 months after release. Set the version in engines and .nvmrc, so CI and developers’ machines use the same one.

What SemVer promises, and what it doesn’t

Semantic Versioning uses MAJOR.MINOR.PATCH. The major number goes up for incompatible API changes, the minor number for new features that are backward compatible, and the patch number for backward-compatible fixes. npm’s version ranges are built on this:

  • ^1.4.2 accepts any 1.x.x version from 1.4.2 up, but not 2.0.0. This is npm’s default when you install a package.
  • ~1.4.2 accepts only patch updates (1.4.x).
  • Before 1.0, the caret is stricter. ^0.4.2 means >=0.4.2 <0.5.0, because in 0.x versions any minor release is allowed to break things.
  • Prerelease versions such as 2.0.0-beta.1 are never matched by an ordinary range, so ^1.4.2 will never pull in a beta. You have to ask for one explicitly, and they rarely belong in production.

SemVer is a promise from the maintainer, though, not a guarantee. Minor releases sometimes break things by accident. A bug fix can also break code that depended on the bug. And in September 2025, attackers who had phished a maintainer’s npm account published malware in chalk, debug and other widely used packages as ordinary patch releases, exactly the kind of update most teams merge without a second look. So “minor and patch updates are usually safe” is a reasonable rule for deciding how much review an update gets. It doesn’t replace tests, and it’s why a few days of cooldown before adopting new releases is worth having.

Don’t update just to update, but don’t drift

You don’t have to adopt every release the day it comes out. If a package works, the new version has no security fix or feature you need, and you’re within one major of the latest release, leaving it alone for a while is a reasonable choice. What you can’t afford is to drift. Skipping one major is cheap, while skipping three means doing three migrations at once, under pressure, usually because a security fix is only available in the newest version. The weekly and monthly routine above keeps you close enough to the latest versions that no single update is a big job.

Deprecated and abandoned packages

npm prints a warning when you install a deprecated package, and the message usually says why it was deprecated and what replaces it. Run npm ls <package> to see which of your dependencies pulls it in. A deprecated transitive dependency is often fixed by upgrading the direct dependency that uses it. If you use the deprecated package directly:

  1. Find the reason. The package might be unmaintained, have a known vulnerability, or have been replaced by a successor. The urgency is different in each case.
  2. Choose a replacement. Start with what the maintainer suggests, and compare alternatives by maintenance activity, size and how much of its API you actually use.
  3. Replace it gradually if it’s used throughout the codebase. A thin wrapper module that’s the only place importing the package lets you switch the implementation in one place.
  4. Write down the change in your docs or ADRs, so nobody reinstalls the old package six months later.

Packages that are abandoned but not formally deprecated are harder to spot: no releases in two years, unanswered issues, a single maintainer who has moved on. The fortnightly dependency review is where you catch them. If you can’t find a replacement, ask in the project’s GitHub discussions or on Stack Overflow. Sometimes someone has already forked the package and kept it maintained.

Supply chain hygiene

Updating often makes it more likely that you’ll pull in a compromised release, so put these protections in place alongside the schedule:

  • Commit the lockfile, and install from it in CI with npm ci (yarn install --immutable or pnpm install --frozen-lockfile). Everyone then builds with exactly the same versions, and CI fails if package.json and the lockfile disagree.
  • Wait a few days before adopting new releases: Dependabot’s cooldown, Renovate’s minimumReleaseAge, or pnpm’s minimumReleaseAge. Most malicious versions are found and removed within hours or days.
  • Turn off install scripts by default (ignore-scripts=true in .npmrc), and allow them only for the few packages that genuinely need a build step. pnpm 10 already does this by default.
  • Run npm audit in CI, and consider a tool like Socket that flags packages behaving suspiciously, such as making network calls, adding install scripts or obfuscating code, even before a CVE exists.

I cover this in more detail in my security best practices post.

Front-end assets outside npm

Scripts and stylesheets loaded from a CDN with a <script> tag never show up in your lockfile, so no bot will ever suggest updating them. List them somewhere, pin exact versions in the URLs, and add a Subresource Integrity hash so the browser refuses a file that has changed:

<script src="https://cdn.example.com/[email protected]/lib.min.js"
        integrity="sha384-..."
        crossorigin="anonymous"></script>

This risk is real. In 2024, the polyfill.io domain changed owners and began serving malicious code to the more than 100,000 sites that still loaded scripts from it. Better still, install these assets through npm and bundle them yourself, so they’re covered by the same lockfile, bot and review schedule as everything else.

Tests that make updates safe

An update schedule is only as good as the tests that run on each update pull request. Unit tests catch API changes in libraries you call directly. Front-end updates more often break things that unit tests can’t see: a component library changes its default styles, a bundler update changes the output, a polyfill disappears and an older browser breaks. A few Playwright end-to-end tests of your main user flows, visual regression snapshots of key pages, and a bundle size budget (size-limit, or your bundler’s own size warnings) catch most of these before users do. The more these checks catch, the more of the weekly review you can leave to CI.

Share...
 

Hamid Mosalla

Hi, I'm Hamid ("Arman"). I'm a software developer with 8+ years of experience in C#, .NET Core, Software Architecture and Web Development. I enjoy creating dev tools, contributing to open-source projects, and sharing insights on my blog. Outside of tech, I’m into indie cinema, classical music and abstract art.

 

Leave a Reply

Your email address will not be published. Required fields are marked *