DEV Community

Cover image for Migrating a Healthcare SaaS from AngularJS to Next.js: What 10 Years of Technical Debt Actually Looks Like
Gennadii Karpunov
Gennadii Karpunov

Posted on

Migrating a Healthcare SaaS from AngularJS to Next.js: What 10 Years of Technical Debt Actually Looks Like

This is the first article in a series about migrating a production healthcare platform from AngularJS to Next.js — not a tutorial, but a war story from the trenches.


How It All Started

In 2015, our team was building a large desktop application for medical imaging — a Java Swing system that could span across multiple monitors, handling thousands of functions for measurements, contouring, 3D modeling, and PACS integrations. It was a serious piece of software.

Then one of our clients — a major university hospital — suggested adding a quality improvement tracking module. The idea was to build it into the existing desktop app, but our team lead at the time pushed back. The imaging application was already overloaded. So we spun off an independent web project.

The stack decision happened while I was traveling. AngularJS was chosen — it was the hot framework of 2015, and the person who pushed for it made a convincing case. Most of our team had deep desktop experience but limited web background. A few of us had touched Sencha Touch and GWT, but nobody was doing modern web development full-time.

By the time I came back, there was already a skeleton: AngularJS on the front, Spring on the back, and Metronic as the UI theme. A new developer had been brought on for frontend, and a designer who was self-taught but surprisingly capable was handling the markup.

I opened the frontend code and... it was rough. Variables named after nothing, copy-pasted demo snippets, no folder structure, hardcoded strings everywhere. It looked like a first-year student project. When I raised this, the response was classic: "That's how everyone does it, look at the examples online."

I pushed for structure. Introduced proper folder organization, naming conventions, extracted shared logic, and — crucially — added i18n from day one to get hardcoded strings out of templates. That last decision paid dividends for years. But the foundation was already "groomed into shape" rather than "built right from scratch." That distinction would haunt us.

Within months, due to normal team turnover, I became the technical lead responsible for the entire platform. Not because of some grand promotion — people just moved on, and I was the one still standing.

Building a Real Product

We delivered the first modules to our client in mid-2016. The initial challenge was migrating their existing data — they had been tracking quality events using a homegrown system that stored everything as text files. About 1,200 of them, each with a loosely defined format. I built a parser that could handle the variations, accept ongoing updates, and deduplicate against new submissions while they transitioned to our platform.

From there, the product grew. Analytics, imports/exports, diagramming, attachments, surveys, and dozens of modules built over years. We picked up several more clients, each with their own requirements that we integrated into the core product.

Healthcare clients brought serious security requirements. We went through multiple rounds of security reviews that fundamentally shaped our architecture:

  • We moved from localStorage to HTTP-only cookies for token management
  • Built a session instance model that supports users working across multiple groups simultaneously
  • Added configurable auto-logout on inactivity, tab abandonment detection, and session expiry warnings
  • For one client, I deployed our entire infrastructure inside their private network via VPN and have maintained it separately for years

Each security review made us better. We cleaned up every potentially vulnerable surface. And one principle I enforced from day one: always validate access levels on the backend, regardless of what the frontend does. This turned out to be one of the best architectural decisions we ever made — when the time came to migrate the frontend, the security layer didn't need to change at all.

The Slow Suffocation

The problems didn't arrive as a crisis. They crept in.

Around 2016-2017, our lists started choking on heavy entities. AngularJS digest cycles would freeze the UI for painful seconds. We patched it with infinite scroll, but after 3-4 pages loaded, we were back to 3+ second hangs.

My first fix was aggressive: inject ReactDOM directly into AngularJS directives and render the heavy lists in React. It worked for the most critical screens, but ReactDOM inline was ugly, hard to maintain, and nearly impossible to extend.

Then one of our developers found a better approach: use Babel to enable JSX directly within our AngularJS project. It required switching to Webpack, and it was a bit hacky in terms of the build pipeline, but the result was clean. We gradually moved most performance-critical lists to JSX. By the time we started seriously discussing migration, about 25% of our frontend was already React via JSX.

But the real killer wasn't performance — it was ecosystem decay.

The hundreds of libraries we depended on in 2015-2016 started dying. Maintainers stopped responding. Updates stopped coming. Bug reports went into the void. For smaller libraries, we forked them and applied minimal fixes ourselves, essentially "baking" frozen dependencies into our codebase.

The larger dependencies were worse. By 2019, npm install was a minefield of version conflicts. Libraries that were actively maintained had moved on to versions incompatible with our ancient dependency tree. Metronic released new versions, but they pulled in newer Bootstrap, which refused to coexist with our 5-year-old libraries.

Our designer spent months — twice — trying to upgrade Metronic. The first time, nothing would merge cleanly against the constantly changing codebase. The second time, a couple years later, he couldn't even get the project to run. Both attempts ended in frustration and wasted effort — a textbook demonstration of why "just upgrade" doesn't work on a living project with deeply entangled dependencies.

Meanwhile, the browser console was becoming a wall of deprecation warnings. More than half of all console output was compatibility warnings of various kinds.

We looked at Angular 2.0 as a potential migration path. The official documentation opened with something along the lines of: "If you're on AngularJS, think carefully before attempting this — it's probably not worth it." Inspiring.

The Pause

Then 2020 hit, and COVID turned everything upside down. You'd think a pandemic would be a boom time for healthcare software. The opposite happened — hospital budgets were slashed, several clients dropped off, and promising leads went cold. For over a year, we operated on minimal capacity. Migration wasn't even on the table. Survival was the priority.

But the ecosystem didn't pause with us. Libraries kept moving forward, the gap kept widening, and the console kept getting redder.

The Point of No Return

After COVID, we needed to update React — the version we used for our JSX components — to support certain component behaviors. The update went through, but React immediately started throwing bright red warnings: the next version would definitively break compatibility with our legacy setup.

This was the hard deadline we'd been avoiding. I put it bluntly to management: the next even minor update may not be possible. We've carried this technical debt too long. It was time.

The AI Migration Experiment

Before going the manual route, we tried a shortcut. A company that specialized in AI-powered AngularJS migrations offered their services with a 100% money-back guarantee.

I was deeply skeptical for three reasons:

  1. Our project structure was non-conventional from 2015, and while we'd improved it over the years, it still had quirks
  2. We had extensive React/JSX islands embedded via directives and ReactDOM — a hybrid that no automated tool would expect
  3. The application had evolved over many years with complex domain-specific features — dynamic theme generation, runtime i18n switching, sophisticated session management — the kind of functionality that emerges organically in a mature product and doesn't map neatly to any template

They promised the entire migration would take two to three weeks. The price wasn't astronomical, and they claimed to have processed 20MB+ codebases for other clients. Ours was about 7MB.

But the decision was made to try. We spent 3-4 months preparing our codebase for their pipeline, then waited another couple of months while they polished their "next-gen" version.

The first output was... code-shaped. Components were vaguely recognizable, but any actual logic was replaced with TODO placeholders. From a hundred-mile journey, this wasn't even the first mile. Their review interface was so buggy that it occasionally showed me data from other customers' projects — not exactly confidence-inspiring for a healthcare context.

Over the next few months, they delivered improved versions roughly monthly — a far cry from the original two-to-three-week promise. Each version was better — but "twice as good" going from 2% to 4% is still nowhere near usable. Their final version was noticeably improved, and they were proud of it. I second-guessed myself for a moment: maybe I'm being too critical? Maybe this is a reasonable starting point?

I asked other developers on the team to review it independently. They confirmed: it couldn't serve as a foundation. We parted ways amicably — they were professional about it, offered us to keep and use whatever we found useful from the generated code, and honored the money-back guarantee. In practice, we didn't use any of it.

The lesson: almost a year of calendar time was spent on the external AI approach. That time could have gone toward manual migration. Automated migration tools may work for conventional projects with standard structures. If your project has years of organically evolved logic, non-standard architecture, and real business complexity — there is no shortcut.

The Strangler Fig Approach

We chose React + Next.js + Metronic 9 as the target stack. Nobody on the team had production experience with this combination, so I took a course on React best practices first — our JSX islands were functional but not idiomatic React.

I started building the new application shell from scratch while the team continued maintaining the existing product. This is the fundamental tension of any migration on a small team: the new thing competes for attention with the old thing that's actually making money.

The breakthrough idea came from an unexpected place. I remembered how PrivatBank — a major Ukrainian bank — migrated their web interface in the 2010s. Some pages opened in the old UI, some in the new. Users barely noticed the transition.

I spent a couple of months trying to explain this concept to management — the idea that we don't need to wait until the entire new frontend is ready before showing progress. Once we actually built it — a dynamic routing layer in nginx that can switch individual pages between the old and new frontend — management immediately saw how powerful it was. Migration progress became visible and tangible, not some abstract "we're working on it" buried in a backlog. We called it simply "the switch."

This is essentially the Strangler Fig pattern, but arrived at through practical inspiration rather than architectural theory. The beauty of it:

  • Both frontends share the same backend and authentication
  • We can migrate page by page, prioritizing the most-used screens
  • Any page can be switched back to the old UI instantly if something breaks
  • Clients see a gradually improving interface without any big-bang cutover
  • The security layer stays untouched since it lives on the backend

For a healthcare platform where downtime and broken workflows are unacceptable, this approach was a lifesaver.

AI as a Tool, Not a Magic Wand

With the migration underway, the team naturally started using AI assistants — Claude, Copilot, ChatGPT — for writing new components. And here's where it got interesting.

When developers who understood the architecture used AI to accelerate their work, the results were solid. AI is excellent at generating boilerplate, converting known patterns, and handling repetitive tasks. The developer stays in control, reviews critically, and integrates thoughtfully.

But when AI-generated code was introduced into the project without sufficient architectural oversight — at several points during the migration — the pattern was always the same. The code looked plausible and appeared to work on the surface, but fell apart under real-world conditions. Developers who inherited this code spent weeks trying to salvage it before we collectively decided it was faster to rewrite from scratch.

The hidden cost of AI-generated code isn't the generation — it's the downstream debugging. Code that "looks right" but has no coherent internal logic is harder to fix than code that's obviously broken. You keep thinking you're one fix away from it working, when actually the foundation is wrong.

One area where this hit especially hard was styling. When our design lead became unavailable, CSS decisions were effectively delegated to AI. Each generated fix added layers on top of layers until our core data table became completely unmanageable — any new fix would either break something else or add noticeable performance overhead. The solution was to go back to the source: manually deconstruct the Metronic table component, understand its internals, strip out everything unnecessary, and rebuild our implementation cleanly. It flies now.

The key distinction we learned: AI in the hands of an engineer who understands the system is a useful tool — imperfect, but manageable. AI as a way to bypass engineering expertise is an expensive lesson.

Coordinating Chaos

In late 2025, we declared a feature lock — two months where all clients were warned to expect only critical fixes while we focused on migration. The reality, as always, was messier.

A major backend refactoring that I'd completed a year earlier turned out to not be the "final version" as originally confirmed — management decided it needed significant rework, right when the migration was supposed to be the sole focus. That refactoring alone was a two-month effort minimum. A new client needed survey functionality. One existing client needed an OS migration. Instead of a focused sprint, we had three parallel workstreams with potential merge conflicts between them.

The backend refactoring affected both the old and new frontends, but the new frontend needed to be built against APIs that didn't exist yet. I had to give the team working on the new UI a forward-looking specification of how things would work once the refactoring was complete. Building against a moving target is never fun, but with good communication it worked.

What didn't work as well: when I stepped away from direct oversight of the new frontend to focus on the backend refactoring, each developer did great work in their own area, but nobody owned the whole picture. Shared component patterns diverged. State management approaches varied between screens. The technical debt of a leaderless frontend accumulated quickly.

When my refactoring finished and I came back to the new frontend, it needed significant consolidation — not because the individual work was bad, but because it lacked coherent direction. I'm currently re-indexing the entire new codebase in my head and systematizing what was built.

Where We Are Now

As of early 2026, we have a working production deployment with the switch mechanism active. The new Next.js frontend handles a growing number of screens, with the AngularJS application still serving the rest. We're about to open the new UI to our clients.

The migration isn't done. It won't be done for a while. But it's alive, in production, and improving steadily — which is exactly how it should work with the strangler fig approach.

What I'd Tell You Over Coffee

If you're sitting on a legacy AngularJS application and thinking about migration, here's what 10 years taught me:

Start i18n early. I added internationalization in 2015 on a hunch. It saved us countless times over the years and made the migration significantly easier.

Your security layer should live on the backend. We enforced backend access validation from day one, independent of the frontend. When we swapped the frontend, the security model didn't need to change. This is probably the single best architectural decision of the entire project.

Don't wait for the perfect moment to migrate. It doesn't exist. Ecosystem decay is gradual, and every month you delay, the gap between your dependencies and the living ecosystem widens. We waited too long — not because we didn't see the signs, but because there was always something more urgent.

The strangler fig pattern works. Don't try a big-bang rewrite. Run both systems in parallel, switch pages as they're ready, keep the old system as a fallback. For healthcare or any system where reliability matters, this is the only sane approach.

AI tools are not magic. They can help, but even with clear instructions they'll produce inconsistent results. They work better when an experienced developer is steering and reviewing. They fail expensively when used to generate code that gets handed off without architectural review. The time saved on generation can be lost many times over on debugging code that looks right but isn't.

Someone must own the architectural vision. On a small team where everyone is busy, it's tempting to let each developer own their area. But a frontend without coherent direction accumulates debt faster than you'd believe — especially with AI-assisted development, where generating inconsistent code is effortless.

Migrations on small teams are never the only priority. Clients need fixes, new features are requested, infrastructure needs maintenance. Plan for the migration to take twice as long as your estimate, and you might be close.


This is part of a series. Next: a deeper technical dive into how we implemented the strangler fig pattern with nginx routing, shared authentication, and zero-downtime page switching between AngularJS and Next.js.


I'm a Software Architect with 17 years in healthcare SaaS, currently navigating an AngularJS → Next.js migration in production. Connect on LinkedIn if you're dealing with similar challenges.

Top comments (0)