<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gennadii Karpunov</title>
    <description>The latest articles on DEV Community by Gennadii Karpunov (@iagenko).</description>
    <link>https://dev.to/iagenko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3827511%2Fd66298b7-dd66-40dd-ad36-9b48250b554c.png</url>
      <title>DEV Community: Gennadii Karpunov</title>
      <link>https://dev.to/iagenko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iagenko"/>
    <language>en</language>
    <item>
      <title>The Strangler Fig in Production: Running AngularJS and Next.js Side by Side</title>
      <dc:creator>Gennadii Karpunov</dc:creator>
      <pubDate>Tue, 24 Mar 2026 10:56:51 +0000</pubDate>
      <link>https://dev.to/iagenko/the-strangler-fig-in-production-running-angularjs-and-nextjs-side-by-side-2f7c</link>
      <guid>https://dev.to/iagenko/the-strangler-fig-in-production-running-angularjs-and-nextjs-side-by-side-2f7c</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the second article in a series about migrating a production healthcare platform from AngularJS to Next.js. The &lt;a href="https://dev.to/iagenko/migrating-a-healthcare-saas-from-angularjs-to-nextjs-what-10-years-of-technical-debt-actually-5df5"&gt;first article&lt;/a&gt; covered how we got here — 10 years of technical debt, ecosystem decay, and a failed AI migration attempt. This one is about the approach that actually works.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Big-Bang Rewrites Were Never an Option
&lt;/h2&gt;

&lt;p&gt;When you run a healthcare SaaS platform used by clinical staff daily, you can't just flip a switch and hope for the best. You can't take the system down for a migration weekend. You can't risk broken workflows for users who are tracking quality events in hospitals. And you definitely can't pause feature development for months while you rewrite everything.&lt;/p&gt;

&lt;p&gt;We knew this from experience. Our designer attempted a Metronic UI framework upgrade twice over the years. Both times followed the same pattern: months of isolated work, then an attempt to merge against a codebase that had moved on without him. The first time, weeks of merge conflicts. The second time — years later — he couldn't even get the project to start. Both attempts were abandoned entirely.&lt;/p&gt;

&lt;p&gt;These weren't failures of effort or skill. They were proof that any migration approach requiring a "stop the world" phase is fundamentally broken for a living product with active development.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Inspiration
&lt;/h2&gt;

&lt;p&gt;The idea came from an unexpected place. I remembered how PrivatBank — one of the largest Ukrainian banks — migrated their web interface sometime in the 2010s. When you logged in, some pages would open in the old design, others in the new. The most common user flows were on the new UI first. Over time, more pages switched over until the old interface simply disappeared.&lt;/p&gt;

&lt;p&gt;It took me a couple of months to fully articulate this concept to management. The abstract idea — "we'll run both frontends in parallel and switch pages gradually" — made sense in theory, but it wasn't clicking. Would users see two different apps? How would authentication work? How would we know which pages are ready?&lt;/p&gt;

&lt;p&gt;Once we actually built a working prototype and deployed it — 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, but real screens that real users could interact with, with instant rollback to the old UI if anything was wrong.&lt;/p&gt;

&lt;p&gt;We called it simply "the switch."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Our Backend Made This Possible
&lt;/h2&gt;

&lt;p&gt;Before diving into the switch implementation, it's worth explaining why swapping the frontend was feasible at all: our backend was effectively frontend-agnostic from day one.&lt;/p&gt;

&lt;p&gt;Over the years, several clients asked for direct API access to build their own integrations. I provided them with API structure documentation and even sample JavaScript snippets with plain AJAX calls — how to authenticate, how to pull data, how to submit records. They built their own tools on top of our backend, completely independent of our UI.&lt;/p&gt;

&lt;p&gt;This meant our backend wasn't coupled to AngularJS in any meaningful way. It was a standalone API that happened to have an AngularJS consumer. Adding a Next.js consumer was architecturally no different from adding another client's custom integration. The frontend is just one of potentially many consumers.&lt;/p&gt;

&lt;p&gt;On top of that, we had enforced backend-first security from the very beginning — every API call validates access levels server-side, regardless of what the frontend claims. This decision, made in 2015, turned out to be one of the most consequential of the entire project. When we swapped the frontend, the security model didn't need to change at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Modular nginx Configuration
&lt;/h3&gt;

&lt;p&gt;The foundation was already in place thanks to our sysadmin's approach to nginx configuration. Instead of monolithic config files, he had structured everything modularly: the main &lt;code&gt;nginx.conf&lt;/code&gt; includes all files from &lt;code&gt;conf.d/&lt;/code&gt;, where each domain gets its own &lt;code&gt;.conf&lt;/code&gt; file. Over 10 years of domain changes, mirror setups, and SSL certificates, this modularity proved invaluable — a programmer's approach to server configuration.&lt;/p&gt;

&lt;p&gt;For the switch, we needed two server blocks: one for the old frontend, one for the new. Both proxying &lt;code&gt;/backend/&lt;/code&gt; to the same application server. The critical difference: the old frontend serves static files from a directory (classic AngularJS), while the new one proxies to a Next.js process on port 3000.&lt;/p&gt;

&lt;p&gt;Here's the simplified structure of the new frontend's server block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;new.app.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# The switch configuration — a single JSON file&lt;/span&gt;
    &lt;span class="c1"&gt;# that both frontends consume&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/frontend-redirect-config.json&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;default_type&lt;/span&gt; &lt;span class="nc"&gt;application/json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;alias&lt;/span&gt; &lt;span class="n"&gt;/srv/nextjs/config/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Backend API — same upstream as the old frontend&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/backend/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:18640/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# WebSocket support&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;"upgrade"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kn"&gt;proxy_cookie_path&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="s"&gt;"/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;HttpOnly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;SameSite=none"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Next.js application&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;"upgrade"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kn"&gt;proxy_cookie_path&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="s"&gt;"/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;HttpOnly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;SameSite=none"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old frontend's server block looks similar for &lt;code&gt;/backend/&lt;/code&gt;, but serves static files directly and carries a decade of accumulated rewrite rules, SAML integration paths, and legacy routing logic. The contrast is striking — and a good reminder of why we're migrating.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared Authentication
&lt;/h3&gt;

&lt;p&gt;Both frontends talk to the same backend, and authentication is handled through HTTP-only cookies — a decision we made years ago during security reviews, long before migration was on the table. Because both server blocks set the same cookie parameters (&lt;code&gt;Secure; HttpOnly; SameSite=none&lt;/code&gt;), a user logged into one frontend is automatically authenticated on the other. No duplicate login, no token juggling.&lt;/p&gt;

&lt;p&gt;There's one catch that cost us some time. The old frontend had been using HTTP-only cookies for authentication for years — this was established during security reviews long ago. But the team building the new frontend somehow implemented authentication differently, bypassing HTTP-only cookies entirely. It worked, so nobody questioned it initially. When I had them align with the established approach — which was non-negotiable for our security standards — authentication between the two frontends broke.&lt;/p&gt;

&lt;p&gt;The problem: our old frontend lived on &lt;code&gt;app.example.com&lt;/code&gt; and the new one on a completely different domain. HTTP-only cookies don't share across different domains — something that wasn't visible with the previous non-standard auth approach. The fix was straightforward: move the new frontend to a subdomain of the same domain (e.g., &lt;code&gt;new.app.example.com&lt;/code&gt;) and set &lt;code&gt;COOKIE_DOMAIN=.example.com&lt;/code&gt; so cookies are shared across subdomains. Simple in retrospect, but it only surfaced when we unified the authentication mechanism properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Single Configuration File
&lt;/h3&gt;

&lt;p&gt;From the beginning, I required one architectural constraint: both frontends must consume a single configuration file. Whoever controls the routing shouldn't need to touch either frontend's code, rebuild anything, or redeploy. Just edit the JSON and the behavior changes.&lt;/p&gt;

&lt;p&gt;The implementation — built by a key team member who deserves significant credit for making the entire switch mechanism work (&lt;a href="https://www.linkedin.com/in/mibal-ua/" rel="noopener noreferrer"&gt;Michael Balakhon&lt;/a&gt;) — uses a JSON configuration file served from the nginx layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.6-test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"old-to-new"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/events/list"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/events/:id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/events/detail/:id"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/submit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/submit/event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"force"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/dashboard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/dashboard"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/settings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/settings"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"new-to-old"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/legacy-settings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/settings"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two sections: &lt;code&gt;old-to-new&lt;/code&gt; (routes that should redirect from the old frontend to the new one) and &lt;code&gt;new-to-old&lt;/code&gt; (routes where the new frontend admits "I don't have this yet" and sends the user back). Both frontends fetch this file on startup and apply it to their routing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two Levels of Control
&lt;/h3&gt;

&lt;p&gt;The redirect service on the old AngularJS frontend checks two conditions before redirecting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;force: true&lt;/code&gt;&lt;/strong&gt; — This page is always served by the new frontend, regardless of user preference. Used for pages that are fully stable and tested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User toggle&lt;/strong&gt; — A "Turn on new UI (Beta)" setting in the user's profile. When enabled, all non-forced routes in the config become active for that user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gives us surgical control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For production clients:&lt;/strong&gt; Only &lt;code&gt;force: true&lt;/code&gt; pages redirect. These are the pages we're confident about — submission forms, core event screens. The majority of users interact primarily with these screens and may never even realize there's an old UI behind them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For test environments:&lt;/strong&gt; Everything redirects to the new frontend. Even half-finished screens. This keeps the team honest about the real state of things.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For individual users:&lt;/strong&gt; If someone hits a bug or needs a feature that hasn't been migrated yet, they flip the toggle off and they're back on the fully functional old UI instantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Graceful Degradation
&lt;/h3&gt;

&lt;p&gt;On both frontends, the redirect logic is designed to fail silently. If the configuration file doesn't load — due to a network issue, a misconfiguration, or anything else — the application simply doesn't redirect. The old frontend works exactly as it always has. The new frontend works standalone. No crashes, no error screens, just a log entry.&lt;/p&gt;

&lt;p&gt;This was a deliberate design choice. In healthcare, "it doesn't work perfectly yet, we're fixing it" is acceptable. "It doesn't work at all" is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Migrated First
&lt;/h2&gt;

&lt;p&gt;The migration order followed user impact, not technical complexity. The core event tracking module — submission forms, event lists, event detail screens — went first. These are the screens that 95% of users interact with daily. For most of them, the new UI is the only UI they'll ever see.&lt;/p&gt;

&lt;p&gt;Power users, administrators, and super users will encounter the old UI for a while longer. Configuration screens, user management, advanced settings — there are hundreds of these, and they'll migrate gradually. But the critical path is already on the new frontend.&lt;/p&gt;

&lt;p&gt;We maintain different configurations for test and production environments. On test, the version string is ahead (e.g., &lt;code&gt;0.6-test&lt;/code&gt;) with more routes pointing to the new frontend. On production (&lt;code&gt;0.4-prod&lt;/code&gt;), we're more conservative — fewer &lt;code&gt;old-to-new&lt;/code&gt; routes, more &lt;code&gt;new-to-old&lt;/code&gt; fallbacks. As screens are tested and stabilized, we promote routes from test to production configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Is a Page "Ready"?
&lt;/h2&gt;

&lt;p&gt;Our approach to readiness is deliberately quality-driven, not deadline-driven. The switch gives us the luxury of not rushing — the old UI works perfectly fine, so there's no pressure to push half-baked screens to production.&lt;/p&gt;

&lt;p&gt;The criterion is simple: when we're fully satisfied with the result, we switch. Not "when it mostly works," not "when we've hit a deadline," but when the page would make a good impression as a finished product. We want users switching to the new UI to feel like they're getting an upgrade, not participating in a beta test.&lt;/p&gt;

&lt;p&gt;This is partly why we're "about to launch any day now" and have been for a little while. We could have switched weeks ago if we were willing to ship something rough. We're choosing not to. In healthcare, first impressions with clinical users matter — if the new UI feels buggy or incomplete, users will switch back and be reluctant to try again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You'll Forget to Migrate
&lt;/h2&gt;

&lt;p&gt;When you plan a page-by-page migration, you think about routes, components, API calls, and styling. You probably won't think about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session timeout logic.&lt;/strong&gt; Our old frontend had years of accumulated behavior around tracking inactive sessions — idle detection, tab close handling, automatic logout after configurable inactivity periods, warning dialogs before timeout. Some of these were added to satisfy specific client security requirements. None of them existed on the new frontend.&lt;/p&gt;

&lt;p&gt;The result: a user on the new UI with strict session settings enabled would get logged out after two minutes of normal use, because the new frontend wasn't sending the keepalive signals that the old one had been sending for years.&lt;/p&gt;

&lt;p&gt;These invisible behaviors — things that aren't features, aren't in any ticket, and aren't in any documentation — are the real migration risk. They live in the old code and in the heads of people who built them. The strangler fig approach at least gives you a safety net: when an invisible behavior surfaces as a bug, the user switches back to the old UI while you fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Page Reload Trade-Off
&lt;/h2&gt;

&lt;p&gt;One thing the switch doesn't do is provide a seamless transition. When a user navigates from an old-UI page to a new-UI page (or vice versa), there's a full page reload. A brief loading flash as the browser navigates between the two subdomains.&lt;/p&gt;

&lt;p&gt;This bothered me initially. I remember thinking during the early shell development that any URL change triggering a full reload felt wrong. Then I realized we'd been living in AngularJS's hash-based routing world for so long that I'd forgotten the real web had moved on — modern frameworks handle URL changes without reloads natively.&lt;/p&gt;

&lt;p&gt;Within each frontend, navigation is smooth. The Next.js shell maintains a persistent layout container, and only the page content swaps. But crossing the boundary between old and new? Full reload. It's not ideal, but it's a trade-off we can live with. Users see a brief "Loading" state, and they're in the other UI. Far better than the alternative of waiting months for a complete rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where We Are Now
&lt;/h2&gt;

&lt;p&gt;The switch is live in production. Core event tracking screens run on the new Next.js frontend. Hundreds of secondary screens still live on the old AngularJS application. Users with the beta toggle can explore more of the new UI, and anyone can fall back to the old UI at any time.&lt;/p&gt;

&lt;p&gt;We're days away from opening the new UI to all users. The core workflows are on Next.js, the switch is stable, and the fallback to the old UI is always one toggle away. Hundreds of screens still need to migrate, and that will take months. But the mechanism is proven, the risk is contained, and every new page we switch over makes the next one easier.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next in the series: what we learned about AI-assisted development during the migration — commercial AI migration services, vibe coding pitfalls, and the real distinction between AI as a tool and AI as a shortcut.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a Technical Lead and Software Architect with 17 years of experience building and maintaining healthcare SaaS platforms. The switch mechanism described in this article was implemented by &lt;a href="https://www.linkedin.com/in/mibal-ua/" rel="noopener noreferrer"&gt;Michael Balakhon&lt;/a&gt;, who turned an architectural concept into a working production system. Connect with me on &lt;a href="https://www.linkedin.com/in/gkarpunov/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; if you're going through something similar.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>nginx</category>
      <category>legacycode</category>
      <category>angular</category>
    </item>
    <item>
      <title>Migrating a Healthcare SaaS from AngularJS to Next.js: What 10 Years of Technical Debt Actually Looks Like</title>
      <dc:creator>Gennadii Karpunov</dc:creator>
      <pubDate>Mon, 16 Mar 2026 15:22:03 +0000</pubDate>
      <link>https://dev.to/iagenko/migrating-a-healthcare-saas-from-angularjs-to-nextjs-what-10-years-of-technical-debt-actually-5df5</link>
      <guid>https://dev.to/iagenko/migrating-a-healthcare-saas-from-angularjs-to-nextjs-what-10-years-of-technical-debt-actually-5df5</guid>
      <description>&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How It All Started
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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: &lt;em&gt;"That's how everyone does it, look at the examples online."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Real Product
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Healthcare clients brought serious security requirements. We went through multiple rounds of security reviews that fundamentally shaped our architecture:&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Slow Suffocation
&lt;/h2&gt;

&lt;p&gt;The problems didn't arrive as a crisis. They crept in.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;But the real killer wasn't performance — it was ecosystem decay.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The larger dependencies were worse. By 2019, &lt;code&gt;npm install&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Meanwhile, the browser console was becoming a wall of deprecation warnings. More than half of all console output was compatibility warnings of various kinds.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pause
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;But the ecosystem didn't pause with us. Libraries kept moving forward, the gap kept widening, and the console kept getting redder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Point of No Return
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Migration Experiment
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I was deeply skeptical for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Our project structure was non-conventional from 2015, and while we'd improved it over the years, it still had quirks&lt;/li&gt;
&lt;li&gt;We had extensive React/JSX islands embedded via directives and ReactDOM — a hybrid that no automated tool would expect&lt;/li&gt;
&lt;li&gt;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&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Strangler Fig Approach
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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."&lt;/p&gt;

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

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

&lt;p&gt;For a healthcare platform where downtime and broken workflows are unacceptable, this approach was a lifesaver.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI as a Tool, Not a Magic Wand
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;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.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Coordinating Chaos
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where We Are Now
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Tell You Over Coffee
&lt;/h2&gt;

&lt;p&gt;If you're sitting on a legacy AngularJS application and thinking about migration, here's what 10 years taught me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start i18n early.&lt;/strong&gt; I added internationalization in 2015 on a hunch. It saved us countless times over the years and made the migration significantly easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your security layer should live on the backend.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't wait for the perfect moment to migrate.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The strangler fig pattern works.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI tools are not magic.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Someone must own the architectural vision.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrations on small teams are never the only priority.&lt;/strong&gt; 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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part of a series. &lt;a href="https://dev.to/iagenko/the-strangler-fig-in-production-running-angularjs-and-nextjs-side-by-side-2f7c"&gt;Next article&lt;/a&gt;: 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.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a Software Architect with 17 years in healthcare SaaS, currently navigating an AngularJS → Next.js migration in production. Connect on &lt;a href="https://www.linkedin.com/in/gkarpunov/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; if you're dealing with similar challenges.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>legacycode</category>
      <category>javascript</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
