Modernise an Older JavaScript Codebase Without Rewriting It
jQuery and AngularJS front ends, class components, CommonJS modules, a Webpack config nobody understands and a Node version that stopped receiving fixes. Replaced route by route, with a working product the whole way through.
A Full Rewrite Is Usually the Wrong Answer
It is also the most common proposal, because it is the easiest one to describe in a meeting.
Rewrite From Scratch
A new codebase is started alongside the old one, with the intention of switching over when it reaches parity. Parity is further away than anyone thinks, because most of the old system is undocumented behaviour.
- Years of edge cases and quiet fixes are discarded and then rediscovered in production
- The old system still needs maintaining, so you fund two codebases at once
- No user sees any benefit until the very end, which makes it politically fragile
- Feature development stalls or gets done twice
- If it is cancelled halfway, you have spent the money and kept the original problem
Replace It Incrementally
The new stack is introduced inside the existing application, and routes move across one at a time behind the URLs users already have. The old system shrinks until there is nothing left of it.
- The product works and deploys at every step
- Benefit arrives with the first route, not at the end of the programme
- Behaviour is preserved because the old code is still there to compare against
- Feature work continues in parallel, in the new stack
- If priorities change, you stop at a coherent point with real value banked
Total effort is usually higher this way than a clean rewrite looks on paper. It is also the reason these projects finish.
When a Rewrite Genuinely Is the Right Call
It happens. It is just far rarer than the number of times it gets proposed.
The Platform Is a Dead End
The runtime, framework or hosting platform no longer receives security fixes and cannot be upgraded in place. At that point you are not choosing a rewrite, you are choosing a deadline.
The Product Is Being Redefined
Not re-skinned — redefined. If the domain model, the pricing and the core workflow are all changing, you are building a different product and preserving the old behaviour is not a goal.
It Is Genuinely Small
An internal tool of a few screens, where a rewrite is a few weeks. Incremental replacement has overhead, and below a certain size that overhead is more than the thing you are replacing.
If you think one of these applies, say so early. We will either agree or explain, specifically, why we think the incremental route is cheaper for you — and a code audit is the usual way to settle the argument with evidence.
The Strangler-Fig Approach
New growth wrapped around the old structure until the old structure is no longer holding anything up.
Draw the Seam
Find where the system can be cut: a route, a page, a service boundary. Put something in front — a proxy, a router rule, a feature flag — that can send a request to either the old or the new implementation.
Pick the First Route
Not the hardest and not the most trivial. Something real enough to prove the pattern, small enough to finish, and low enough risk that being wrong is survivable. Often a page that is already being changed for other reasons.
Rebuild and Switch
The new implementation ships behind the same URL, released to a slice of traffic first where that is possible. The old code stays in place, so comparing behaviour and rolling back are both trivial.
Repeat, Then Delete
Route by route, with feature work happening in the new stack all the while. Dead code is removed as soon as nothing routes to it, because a half-migrated system with both versions live is its own kind of debt.
The same shape works on the backend: put the new service behind the existing API, move endpoints across, retire the old handler once traffic is gone.
What We Modernise
Rarely just one of these. They tend to arrive together, and the order they are tackled in matters more than the individual choices.
jQuery to a Modern Framework
- Usually React or Vue, mounted into part of an existing page rather than replacing the whole front end on day one
- Both can coexist with jQuery in the same page, which is what makes screen-by-screen replacement possible
- The hard part is never the framework: it is the state spread across DOM attributes, globals and event handlers
- Worth noting that jQuery in a page that works is not by itself a problem. Hiring, security fixes and the cost of each new feature are the reasons to move
AngularJS
- The framework is long past end of life, so this is the case where the security argument is straightforward
- Route-by-route replacement, with the two frameworks coexisting behind the same shell while the migration runs
- Scopes, watchers and two-way binding rarely map cleanly onto a modern component model, so the data flow gets redesigned rather than translated
- Services and business logic often survive almost unchanged, which is where most of the saving comes from
Class Components to Hooks
- Not urgent. Class components still work, and converting them wholesale is effort with little return
- Convert the ones you are already touching, and the ones whose lifecycle logic has become genuinely hard to follow
- Lifecycle methods do not map one-to-one onto effects, and treating an effect as a lifecycle hook is how subtle bugs get introduced during a conversion
- Shared logic in higher-order components and render props is where the real win is, because a custom hook is simply clearer
CommonJS to ESM
- Increasingly forced rather than chosen, because more packages now publish ESM only
- The mechanical part is straightforward; the difficult part is conditional requires, circular dependencies and anything that mutates module exports at runtime
- Mixed graphs are where the pain concentrates, so this is best done as a deliberate pass rather than file by file over a year
- Tooling, configuration files and test runners usually need attention at the same time, which is why this rarely stays a small task
Webpack to Vite
- Worth doing when the config has become unmaintainable or the feedback loop is slow enough to change how people work
- Not worth doing to a Webpack setup that is small, understood and building fine
- The friction is in loaders and plugins with no direct equivalent, environment variable handling, and anything that depended on CommonJS resolution
- Usually done alongside the module migration above, because the two problems are the same problem
Node Version Upgrades
- One major version at a time, with the test suite run at every step. Jumping several at once turns a sequence of small problems into one unreadable failure
- Native modules need rebuilding and some will no longer compile at all
- Removed globals and internal APIs that packages quietly depended on are a frequent cause of breakage
- Old build tooling can fail on stricter module resolution or on OpenSSL-related changes, which is a well-worn trap
Abandoned Packages With No Maintained Replacement
Four honest options. Which one is right depends entirely on how central the package is to what you sell.
Replace It
A maintained equivalent exists and the migration is bounded. Always the first thing to check, and more often available than teams assume once someone actually looks rather than searching once and giving up.
Wrap It Behind Your Own Interface
Put a thin module of your own in front of the dependency so your code calls your interface. The day a replacement appears, or the day it finally breaks, you change one file instead of two hundred call sites. Worth doing even when you are not replacing it yet.
Vendor the Source
Copy a small package into your repository and maintain it as your own code. Unfashionable, and frequently the correct answer for something modest that simply needs to keep working and will never be updated again by anyone else.
Fork and Maintain
Justified only when the package is genuinely core to your product and none of the above works. You are now the maintainer, including for security fixes. What is never an option is leaving an unmaintained package with a known vulnerability at the centre of the system.
What Makes These Projects Go Wrong
The failures are predictable, which means most of them are avoidable.
The first two are usually addressed before any modernisation starts: a thin layer of tests over the journeys that must not change. See testing and automation.
Scoped Work, or a Developer Inside Your Team
Both happen. Which one suits depends on whether this is a project with an end or a permanent change in how you build.
As Fixed-Scope Work
- Scoped after reading the code, not before. A fixed price for a defined stretch — the build migration, the Node upgrade, the first set of routes.
- Delivered as pull requests into your repository, reviewed by your team, in your normal workflow.
- Sequenced so that each piece lands independently and nothing depends on the whole programme completing.
- Usually preceded by a code audit, because the order of work matters more here than anywhere else.
As a Developer Who Joins You
- One or two people who work inside your team, on your board, doing modernisation alongside the features your business still needs.
- Part-time or full-time, with the same two-week replacement guarantee as any other engagement.
- Better when the work will take quarters rather than weeks, or when your own developers should be learning the new stack as it goes.
- See engagement models for how that is structured.
Where to Go From Here
Code Audits
What is actually wrong, in what order it matters, and what each fix would cost. The usual first step.
Learn moreTypeScript Migration
Often runs alongside modernisation, because both are about making change safe again.
Learn moreTesting and Automation
The safety net that makes replacing a route survivable. Usually built before the modernisation starts.
Learn moreHire React Developers
For the screens being rebuilt, and for the team that will own them afterwards.
Learn moreHire Node Developers
For the backend half: endpoint by endpoint behind the API you already publish.
Learn moreRates
Developer rates per month and per hour, and how fixed-scope work is quoted separately.
Learn moreQuestions About Legacy Modernisation
Should we just rewrite it instead?
Almost certainly not, and we will argue against it before we agree to it. A working system encodes years of decisions, edge cases and quiet bug fixes that nobody wrote down, and a rewrite starts by throwing all of that away and rediscovering it in production. Meanwhile the old system still needs maintaining, so you fund two codebases and ship features in neither. A rewrite is genuinely right in a narrow set of cases: the platform is unsupported and cannot be upgraded, the product is being fundamentally redefined rather than re-skinned, or the system is small enough that a rewrite is a few weeks rather than a year. Outside those, replacing it piece by piece gets you the same destination with a working product the whole way.
Can you modernise the codebase while we keep shipping features?
That is the entire point of working this way. New code goes into the modern stack, old routes are replaced one at a time behind the existing URLs, and the application stays deployable at every step. Your team keeps working on features in parallel because nothing is frozen and there is no long-lived branch to merge. It takes longer in total than a clean-room rewrite would take on paper, and it is the reason these projects finish rather than being quietly abandoned two quarters in.
What do we do about dependencies that are abandoned with no replacement?
There are four honest options and the right one depends on how central the package is. Replace it, if a maintained equivalent exists and the migration is bounded. Wrap it behind an interface of your own, so the day you do replace it you change one file rather than two hundred call sites. Vendor it — copy the source into your repository and own it — which is often correct for a small package that simply needs to keep working. Or fork and maintain it, which is the most expensive and only justified when the package is genuinely core to your product. What is not an option is leaving an unmaintained package with a known vulnerability at the centre of the system and hoping.
We are on an unsupported Node version. What actually breaks when we upgrade?
Usually not your own code — it is the dependencies and the build. Native modules have to be rebuilt and some will no longer compile. Packages that relied on internal APIs or removed globals break. Older build tooling can fail on stricter module resolution, and OpenSSL-related changes have caught out plenty of teams whose bundler used an old hashing default. The safe sequence is to upgrade one major version at a time, run the test suite at each step, and fix the dependency tree before touching application code. If the suite is thin, the first piece of work is usually tests around the paths you cannot afford to break.
Do we have to move off jQuery or AngularJS at all?
Not on principle. If the application is stable, rarely changed and secure, jQuery in a page that works is not a business problem. The reasons to move are concrete: you cannot hire for it, the framework itself no longer receives security fixes, every new feature costs three times what it should, or the code has grown past what direct DOM manipulation can keep coherent. If none of those apply to you, spend the money elsewhere. If two or three of them do, the answer is still to replace it screen by screen rather than all at once.
Tell Us What You Are Still Running
The framework, the Node version, the build and the part everybody is afraid to touch. We come back with a sequence, a scope and a fixed price for the first stretch.