Sunsetting Legacy Angular: How We're Migrating to Next.js, GraphQL, and a Monorepo (Without a Big Bang Rewrite)

Published: (February 12, 2026 at 07:26 PM EST)
6 min read
Source: Dev.to

Source: Dev.to

Cover image for Sunsetting Legacy Angular: How We're Migrating to Next.js, GraphQL, and a Monorepo (Without a Big Bang Rewrite)

Overview

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

  • Monolithic architecture – One root module with 637 declared components made the codebase hard to reason about.

  • Manual dependency injection – A custom HTTP service was manually instantiated in 60+ places, bypassing Angular’s DI.

  • Tight coupling – Components were directly tied to specific API shapes.

  • Limited reusability – UI components were Angular‑specific and couldn’t be reused elsewhere.

  • Slow builds – Build times kept growing with the app.

  • Technology debt

    - Angular 14
    - Bootstrap 4
    - jQuery dependencies

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

  • 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/
│   ├── auth-service/
│   ├── graphs/
│   ├── services/
│   └── notification-service/
├── packages/
│   ├── design-system/
│   ├── authentication/
│   ├── logger/
│   ├── database/
│   └── 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 domain‑based GraphQL services.

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.

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);

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 };
};

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,
  });
}

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,
      },
    });
  }
}

Migration Workflow

Step 1: Build in React

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

  if (loading) return null;

  return (
    <div>
      <h2>{data.title}</h2>
      {/* Feature implementation */}
    </div>
  );
};

Step 2: Wrap as Web Component

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

customElements.define('app-feature', FeatureWC);

Step 3: Use in Angular

import '@company/wc-feature';

Step 4: Feature Flag

(Implementation of the flag is handled in the Angular configuration; the HTML placeholder has been omitted for brevity.)

Step 5: Remove Old Code

  • Remove flag
  • Delete Angular component
  • Clean up services
  • 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 modernize incrementally while keeping the business running smoothly.

The strangler‑fig pattern really does work. There’s a middle path between rewriting everything and living with legacy forever.

Technical Stack Summary

Legacy

  • Angular 14
  • Bootstrap 4 + jQuery
  • REST APIs
  • Monolithic architecture

Modern

  • React 18+, Next.js 15
  • NestJS + GraphQL
  • Prisma + SQL Server
  • Turborepo + pnpm
  • Web components via @r2wc
  • Tailwind + Radix UI
  • TypeScript everywhere
  • GitHub Actions + Docker
0 views
Back to Blog

Related posts

Read more »