Skip to content

Insights

Leveraging generative AI for modernisation: Part 1 – Sunsetting legacy Angular frontend

Ujjavala Singh
Ujjavala Singh

If you’ve worked on a long-lived frontend, you already know the story. The app grows, features pile up, deadlines keep coming, and suddenly you’re sitting on a mountain of technical debt. That’s exactly where we were.

We had a large Angular 14 application with 600+ components, a monolithic structure, and increasing complexity that was slowing down development. Behind it ran a large .NET backend, powering critical workflows like user registration, subscriptions, and reporting. A full rewrite of either the frontend or backend sounded tempting, but also risky, expensive, and disruptive to the business. Rather than attempt a “big-bang” rewrite, we designed a migration strategy that let us incrementally replace the legacy code while keeping the app running. 

For clarity, I’ve split this story into two posts:

  • Part 1 (this post) covers the frontend modernisation, how we incrementally replaced Angular components, introduced React via web components, leveraged GraphQL, and structured our monorepo. 

In this post, I’ll walk through the architecture, the migration approach, and the lessons I’ve learned so far on the frontend side, including how generative AI helped us analyse and navigate our legacy code.

The legacy landscape: what we started with

Our Angular application has been the backbone of our business for years.
It handled:

  • Customer and user registration workflows
  • Service provider search
  • Payment processing
  • Role-based user management
  • Complex multi-step forms
  • AWS Cognito authentication

It worked. It delivered value. But it had started to show its age.

Key challenges with the legacy system
  1. Monolithic architecture: One root module with 637 declared components made the codebase hard to reason about.
  2. Manual dependency injection: A custom HTTP service was manually instantiated in 60+ places, bypassing Angular’s DI.
  3. Tight coupling: Components were directly tied to specific API shapes.
  4. Limited reusability: UI components were Angular-specific and couldn’t be reused elsewhere.
  5. Slow builds: Build times kept growing with the app.
  6. Technology debt: The app had grown organically across multiple environments (dev, test, uat, prod). A full rewrite would likely take 12-18 months and carry serious business risk.

So we needed a safer approach.

Leveraging generative AI

I’ve leaned on generative AI to make navigating our legacy Angular code less painful. The stats in this blog, like which components are purely legacy versus newly added are analysed by Claude. We also use Claude in “MD mode” to locate and fix tricky bugs in legacy code, which can be hard to reason about manually. A typical prompt might be something like:

Claude, here’s this legacy Angular component. Explain how it’s used across the app and which user roles have access to it. Highlight any dependencies that might be risky if we migrate this feature.

Most of our experiences with Claude, including detailed examples of how it helps with code analysis and debugging, are in my previous post: A week with Claude Code: lessons, surprises and smarter workflows

How the pieces fit together

At a high level:

  • Angular continues to run the legacy UI.
  • New features are built in React.
  • React apps are shipped as web components.
  • GraphQL sits between frontend and backend.
  • Next.js handles authentication.

This lets us replace features one at a time without disrupting the
business.

Our migration strategy: Strangler fig pattern

We adopted the Strangler Fig pattern, gradually replacing parts of the system while the old one keeps running.

Our approach had three core pillars:

  1. Monorepo foundation
  2. GraphQL-based APIs
  3. Web components as a bridge

Monorepo with turborepo

We built a monorepo using pnpm and Turborepo.

                        monorepo/
├── apps/
│   ├── infra/
│   ├── graphs/
│   ├── services/
├── packages/
│   ├── authentication/
│   └── web-components/
                    
Benefits
  • Shared code across apps
  • End‑to‑end TypeScript
  • Faster builds (70% improvement)
  • Atomic cross‑stack PRs
  • Coordinated versioning

GraphQL as the API layer

Instead of a monolithic REST API, we created a domain‑based GraphQL services.

                        TypeScript

type Business {
  id: ID!
  name: String!
  subscriptionPlans: [Plan!]!
  defaultPlanId: Int
}

type Query {
  searchBusinesses(country: String!, searchTerm: String!): [Business!]!
}
                    
Advantages
  • Clear domain separation
  • Independent deployments
  • Strong typing
  • Efficient client‑driven queries
  • Federation‑ready architecture

Web components: React inside Angular

We used web components to embed React features into the Angular app.

                        TypeScript

import { r2wc } from '@r2wc/react-to-web-component';
import { UserRegistrationWithApollo } from './UserRegistration';

const UserRegistrationWC = r2wc(UserRegistrationWithApollo, {
  props: {
    graphApiUrl: 'string',
  },
});

customElements.define('user-registration', UserRegistrationWC);
                    

Web components allow React features to run inside Angular without rewriting everything. This enables gradual migration, framework-agnostic UI, and reuse across apps.

Angular usage
                        JavaScript

<user-registration
  [graphApiUrl]="apiUrl"
</user-registration>
                    

Why this worked

  • Framework‑agnostic UI
  • Incremental migration
  • Modern React patterns
  • Reusable across apps

Next.js for authentication

                        TypeScript

export async function POST(request: Request) {
  const { refreshToken } = await request.json();
  const newTokens = await refreshCognitoToken(refreshToken);

  return Response.json({
    accessToken: newTokens.accessToken,
    idToken: newTokens.idToken
  });
}
                    

Why Next.js

  • API routes for auth
  • Docker‑ready builds
  • Shared between Angular and React
  • Future‑proof for migration

Migration workflow

Step 1: Build in React
                        TypeScript

export const Feature = () => {
  const { data, loading } = useQuery(GET_DATA_QUERY);

  if (loading) return <Spinner />;

  return (
    <Card>
      <CardHeader>
        <CardTitle>{data.title}</CardTitle>
      </CardHeader>
      <CardContent>
        {/* Feature implementation */}
      </CardContent>
    </Card>
  );
};
                    
Step 2: Wrap as web component
                        TypeScript

const FeatureWC = r2wc(FeatureWithApollo, {
  props: {
    apiUrl: 'string',
    userId: 'string'
  }
});

customElements.define('app-feature', FeatureWC);
                    
Step 3: Use in Angular
                        JavaScript

import '@company/wc-feature';

<app-feature
  [apiUrl]="apiUrl"
  [userId]="currentUser.id">
</app-feature>
                    
Step 4: Feature flag
                        JavaScript

<app-feature *ngIf="featureFlags.useNewFeature"></app-feature>
<legacy-feature *ngIf="!featureFlags.useNewFeature"></legacy-feature>
                    
Step 5: Remove old code
  1. Remove flag
  2. Delete Angular component
  3. Clean up services
  4. Update tests

This step-by-step workflow shows how we gradually replace Angular components with React features via web components. Feature flags allow safe rollout, and old code is removed only once the new implementation is stable.

Final thoughts

Sunsetting a legacy frontend doesn’t have to be scary or require a risky rewrite. By approaching it incrementally, we were able to modernise our Angular app step by step, keeping the business running smoothly while giving developers the freedom to experiment and improve features safely. 

Leaning on a monorepo, GraphQL, web components, Next.js, and Turborepo, we could replace frontend features one at a time, introducing modern patterns without disrupting users. 

In Part 2, I’ll continue the journey by exploring backend modernisation, showing how we applied the same incremental approach to legacy .NET services, introduced GraphQL and Prisma, and leveraged generative AI to navigate complex code, all while modernising safely and keeping production stable.

Let’s make it happen

Tell us where you’re at and we’ll map the buildable next step.
A DiUS specialist will reply within one business day.