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

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:
- Monorepo foundation
- GraphQL‑based APIs
- 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