npm Outdated Dependencies in a Five Year Old Frontend: Where to Start

#npm outdated dependencies

Founder & Lead Developer

Expert in software development and legacy code optimization

LinkedIn

Run npm outdated on a frontend that has not been touched in five years and the terminal fills with more red text than you expected. A hundred packages, maybe two hundred. Half of them are three major versions behind. The instinct is to close the terminal and open a ticket for "full dependency upgrade," then watch that ticket sit in the backlog for another year because nobody wants to own a project with no clear end point.

That instinct is understandable, but it treats every outdated package as the same problem. It is not. Npm outdated dependencies in a years-old React app typically fall into three different categories, and they do not carry the same risk or the same cost to fix. Working through them in the wrong order is how a two-week cleanup turns into a two-month one.

Sorting npm outdated dependencies into three buckets

Run npm audit alongside npm outdated and you get two overlapping but different pictures. npm outdated tells you how far behind you are. npm audit tells you which of those gaps are actually dangerous right now. The two lists need to be read together, because a package can be ten versions behind with zero known vulnerabilities, or one version behind with a critical one.

Sort the full list into three buckets before touching anything:

Security patches are packages with a published CVE against the version you are running, where a fixed version already exists. These are usually small, targeted releases, a patch version bump, not a major one, because maintainers do not bundle a security fix with breaking API changes. Lodash, minimist, and axios have all shipped exactly this kind of patch release in the past few years: same API, one closed vulnerability.

Build tooling covers webpack, Babel, ESLint, your bundler, your test runner, and anything else that touches the codebase without shipping to the browser. These tend to lag furthest behind in an old project because nobody notices a stale dev dependency the way they notice a broken feature. They are also usually the safest category to update, since a build tool failing shows up immediately as a red CI run, not as a bug a customer reports three weeks later.

The framework and its direct runtime dependencies are everything the application actually imports and ships, React, your router, your state management library, your UI component kit. These carry the most risk per update and the most value once done, because an old framework version is what eventually blocks you from using a library, a hosting feature, or a hire who only wants to work in the current version of the ecosystem.

Each category also fails differently if you get the update wrong. A broken security patch is nearly impossible, since maintainers do not ship breaking changes in a patch release. A broken build tool update fails your CI pipeline, which you will see before you merge. A broken framework update can pass every existing test and still produce a subtly wrong page that a user will find before your test suite does, because the behavior change was never covered by a test in the first place.

Why security patches come first, and why that is not a judgment call

Of the three categories, security patches are the only one with a clock attached. OWASP has listed vulnerable and outdated components as one of its top web application risks for years running, and the mechanism is almost always the same: a CVE gets published, the fix is public, and from that point on the gap between your version and the patched one is visible to anyone who checks your dependency manifest or your HTTP response headers. You are not guessing whether an attacker could find this. The fix is published alongside the problem.

The good news is that this category is usually fast. Run npm audit fix first. It handles the patch-level bumps automatically and will not touch anything that requires a major version jump. What is left after that command finishes is the smaller, harder set: vulnerabilities where the fix requires a breaking upgrade, which is where the framework-level work below comes in. But the automatic pass alone typically clears most of the list in a single afternoon, with no code changes required beyond the lockfile.

Build tooling second, because it fails loud instead of quiet

Once the security patches are handled, build tooling is next, not because it is more important than the framework, but because it is cheaper to get wrong. A webpack major version upgrade that breaks something will break your build. You find out in CI, before anything ships, and the fix is usually a config change documented in the tool's own migration guide.

This is also where the oldest, most neglected packages usually live. A frontend that has not been maintained in five years is often still running Webpack 4 or an ESLint config from before flat config existed. These tools get ignored because they are invisible when they work. Nobody opens a ticket for "our bundler still works fine," even when it is years out of date, because nothing about the user experience depends on it directly.

Update one tool at a time rather than all at once. Bump the bundler, run the full build and test suite, commit. Bump the linter config, run it, commit. Treating this as a single giant upgrade is how a one-day task turns into a week of untangling which of six simultaneous changes broke the build.

The framework and its runtime dependencies, last and most carefully

This is the category where the five year gap actually hurts, and where most of the remaining effort goes. If you are three major React versions behind, you are not doing one upgrade, you are doing three in sequence, each with its own deprecated APIs, each documented in a separate migration guide, each needing its own round of manual testing because the automated suite was written for the old behavior and will happily pass against a subtly broken new one.

The React team, like most framework maintainers, documents breaking changes release by release rather than skip-version by skip-version, so plan to go through each major version's migration guide in order rather than looking for a single guide that covers three versions at once. Expect to find deprecated lifecycle methods, removed APIs that your codebase still calls, and peer dependency conflicts where your state management library or UI kit has not caught up to the React version you are moving toward. Check each major dependency's own changelog for framework compatibility before you start the jump, not after you hit the conflict.

If the frontend was built on something older than React itself, jQuery-era code still living inside a modern build, the upgrade path looks different again. We cover that specific situation, migrating piece by piece rather than in one rewrite, in our guide to incremental frontend modernization from jQuery to React. The prioritization logic is the same one described here. The difference is scale, not approach.

A checklist for the first week

Start with npm audit fix to clear the automatic security patches, then review what npm audit still lists afterward, since what remains needs a manual major-version bump rather than an automatic one. Next, check npm outdated for the devDependencies, your build tools and linters, and update those one at a time, running the full test suite between each change. Only then move to the framework and its direct runtime dependencies, going version by version through each migration guide rather than attempting the full jump at once. Along the way, delete anything the codebase no longer imports. A five year old package.json usually has packages nobody has called in years, and removing them before updating means you are not spending time fixing an upgrade for code that does not run anymore.

None of these steps require special tooling beyond what npm already ships. What they require is doing them in this order, so that a failure in one category does not get confused with a failure in another, and so the highest-risk work happens last, once the codebase is already stable enough to absorb it.

When the gap is too wide to close on the side

Some codebases have been left long enough that this becomes more than a week of focused work, dozens of packages with interlocking peer dependency conflicts, a framework version old enough that current documentation barely references it, and a test suite too thin to catch what a major version bump might break. At that point the question stops being which package to update next and becomes whether the codebase needs a structural look before any of this is safe to attempt.

That is the kind of assessment a code quality audit is built for: an honest read on how much of the risk is the dependency list and how much is the code underneath it. Where the audit turns up real structural debt, a legacy code optimization engagement is the next step, working through exactly the kind of staged, test-covered upgrade described above.

If your own npm outdated list has reached the point where it feels safer to ignore than to touch, that is worth a conversation rather than another year of postponing it. Reach out at hello@wolf-tech.io, or take a look at what we do at wolf-tech.io. Most of these cleanups take less time than the backlog makes them look like they will.