How We’re Surviving 600+ Legacy Angular Components While Migrating to Next.js, GraphQL, and a Monorepo

How We’re Surviving 600+ Legacy Angular Components While Migrating to Next.js, GraphQL, and a Monorepo

# graphql# nextjs# angular# webdev
How We’re Surviving 600+ Legacy Angular Components While Migrating to Next.js, GraphQL, and a Monorepoujja

Introduction If you've worked on a long-lived frontend, you already know the story. The...

Introduction

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. A full rewrite sounded tempting, but also risky,
expensive, and disruptive to the business.

So instead of going for a big-bang rewrite, we designed a migration
strategy that let us incrementally replace the legacy code using:

  • Next.js
  • GraphQL federation
  • A monorepo architecture
  • Web components as a bridge between frameworks

This post walks through the architecture, the migration approach, and
the lessons we've learned so far.

The Legacy Landscape: What We Started With

Our Angular application had 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

-   Angular 14
-   Bootstrap 4
-   jQuery dependencies
Enter fullscreen mode Exit fullscreen mode

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.

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 the frontend and the 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/
│   ├── auth-service/
│   ├── graphs/
│   ├── services/
│   └── notification-service/
├── packages/
│   ├── design-system/
│   ├── authentication/
│   ├── logger/
│   ├── database/
│   └── web-components/
Enter fullscreen mode Exit fullscreen mode

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.

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

type Query {
  searchBusinesses(country: String!, searchTerm: String!): [Business!]!
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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

customElements.define('user-registration', UserRegistrationWC);
Enter fullscreen mode Exit fullscreen mode

Angular usage

<user-registration
  [businessGraphApiUrl]="businessApiUrl"
  [providerGraphApiUrl]="providerApiUrl">
</user-registration>
Enter fullscreen mode Exit fullscreen mode

Why this worked

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

Apollo Client: Multi‑Graph Communication

export const createApolloClients = (
  businessUri: string,
  providerUri: string
) => {
  const businessClient = new ApolloClient({
    link: authLink.concat(httpLink(businessUri)),
    cache: new InMemoryCache(),
  });

  const providerClient = new ApolloClient({
    link: authLink.concat(httpLink(providerUri)),
    cache: new InMemoryCache(),
  });

  return { businessClient, providerClient };
};
Enter fullscreen mode Exit fullscreen mode

Next.js for Authentication

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
  });
}
Enter fullscreen mode Exit fullscreen mode

Why Next.js

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

Database Layer: Prisma + SQL Server

@Injectable()
export class DataService {
  constructor(private prisma: PrismaClient) {}

  async getBusiness(id: number) {
    return this.prisma.business.findUnique({
      where: { id },
      include: {
        subscriptionPlans: true,
        locations: true
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Migration Workflow

Step 1: Build in React

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Wrap as Web Component

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

customElements.define('app-feature', FeatureWC);
Enter fullscreen mode Exit fullscreen mode

Step 3: Use in Angular

import '@company/wc-feature';

<app-feature
  [apiUrl]="apiUrl"
  [userId]="currentUser.id">
</app-feature>
Enter fullscreen mode Exit fullscreen mode

Step 4: Feature Flag

<app-feature *ngIf="featureFlags.useNewFeature"></app-feature>
<legacy-feature *ngIf="!featureFlags.useNewFeature"></legacy-feature>
Enter fullscreen mode Exit fullscreen mode

Step 5: Remove Old Code

  1. Remove flag
  2. Delete Angular component
  3. Clean up services
  4. Update tests

Key Metrics After 6 Months

  • 15 major features migrated
  • 70% faster builds
  • 40% less duplicate code
  • 80% of new features built in React
  • Zero migration‑related incidents

Final Thoughts

Sunsetting a legacy app doesn't have to mean a risky rewrite.

By combining:

  • A monorepo
  • GraphQL
  • Web components
  • Next.js
  • Turborepo

...we've been able to modernise one feature at a time while keeping the business running smoothly and making life easier for developers.

The strangler fig pattern really works. It is a framework for thinking about incremental migration instead of forcing a “big bang” rewrite or living with legacy forever. Each new feature in React, each GraphQL service, each shared package brings more value immediately while paving the way for the next step.

What we’ve realised is that modernisation is not just about technology. It’s about workflow, confidence, and developer experience. You can gradually introduce modern tools and patterns, reduce duplicate code, improve type safety, and simplify builds, all while delivering the features your business actually needs.

At the end of the day, you do not have to choose between chaos and stagnation. Incremental migration lets you move forward with clarity, modernise without drama, and build a foundation that can grow with your team and your users for years to come.