Migrating an Angular app to React with an agent: design first, plan first, route by route
A staged approach to moving an Angular app to React with an agent: audit the current UX, redesign, write the migration plan before any code, keep the Angular app read-only, execute one route at a time against a checklist, and keep a changelog.
A framework migration goes wrong when it is a big-bang rewrite. The safe version is a strangler migration: stand up a new React app next to the Angular one, move it over one route at a time, and run both behind a single router until the Angular app is empty. An agent makes each route faster. It does not remove the need to plan the order, define parity, or own the design.
The shape of the migration#
- Phase 0. Audit the current app and redesign. Humans lock the design decisions.
- Phase 1. Write the migration plan as a document, with a route-by-route checklist, before any code.
- Phase 2. Execute the plan one route at a time. Tick the boxes, keep a changelog, flip traffic per route.
Each phase produces an artifact the next phase depends on: a design decisions doc, then a plan, then a growing changelog. If you resume after a week, those three files tell you and the agent exactly where things stand.
Repo layout and the one hard guardrail#
The React app goes in its own directory or its own repo. The Angular codebase stays exactly as it is. The agent reads Angular for behaviour and API contracts. It writes only React.
- Put a rules file at the React app root (a
CLAUDE.mdor equivalent) that states, in plain terms: the Angular directory is read-only reference, never edit, never delete, never move a file inside it. - Repeat the same rule in the plan's guardrails section, because that is what the agent reads per step.
- If your tooling supports it, scope write permission to the React directory so a stray edit to Angular is blocked, not just discouraged.
- Keep the two apps as separate build targets. No shared
node_modules, no shared config that a React change could break.
/legacy-angular/. It is reference only. If a change there seems necessary, STOP and ask." is a rule an agent will hold.Phase 0: design audit and redesign#
Before anything is ported, get an honest picture of what exists and what is worth changing.
Inventory
Point the agent at the running app (screenshots, a screen recording, or the Angular templates and routes) and ask for a screen inventory. For every route: its purpose, the components on it, the user flows that pass through it, the forms and their validation rules, the empty, loading and error states, and the data each screen needs.
UX pitfalls
Then ask it to list the problems it can see: inconsistent spacing and typography, unclear affordances, forms with no inline validation, flows that dead-end, tables too dense to scan, modal chains, missing keyboard support, low-contrast text. This is a first pass, not a verdict. A person confirms which ones are real.
Redesign proposal
Ask for a modernisation proposal: a small design system (tokens, the component set), a cleaner information architecture where the current flows are clunky, and a per-screen note of keep, change, or drop. A human reviews this and locks the decisions into a short design-decisions.md. That file is an input to the plan. Nothing in Phase 2 gets to relitigate it.
Phase 1: the migration plan, written before code#
Give the agent the full context in one place: the screen inventory, the locked design decisions, the route list, the API contracts (an OpenAPI spec or a hand-written list), the shared concerns (auth, i18n, feature flags, analytics), the target stack (React version, router, data layer, styling approach, test runner), and the guardrails. Then ask it to produce a detailed plan as a markdown document. No code yet.
The plan should contain:
- Target architecture. Folder structure, routing model, data-fetching layer, state approach, styling, testing.
- The shared foundation to build first. Design system, auth, the layout shell, the routing skeleton, the API client, test setup. Nothing route-specific ships until this is in place.
- Route-by-route order. Low-risk leaf routes first, shared-heavy routes next, the riskiest (checkout, admin, anything touching money or permissions) last.
- A per-route checklist (below).
- A parity definition. What "the same" means for this app.
- Cutover and rollback. How traffic moves to a migrated route and how it moves back in one step.
- Not in scope. An explicit list, so scope creep has to be a decision, not a drift.
## Route: /invoices (list)
Risk: low. Depends on: design system, API client, auth shell.
- [ ] Read the Angular route: components, services, guards, resolvers.
- [ ] List every API call it makes and the shapes it expects.
- [ ] List validation rules and permission checks.
- [ ] Build the React screen using ONLY design-system components.
- [ ] Port filters, sorting, pagination. Match URL query params.
- [ ] Cover empty / loading / error states per design-decisions.md.
- [ ] Tests: render each state, filter, paginate, permission-denied.
- [ ] Manual parity check against the Angular route (same data, same actions).
- [ ] Note any deliberate UX changes in the changelog.
- [ ] Changelog entry added.
- [ ] Commit. Flip /invoices to React behind the route flag.
- [ ] Watch errors for 24h before removing the Angular route.Review the plan. Edit the order, tighten the guardrails, cut anything speculative. Once it is right, it is the source of truth, and its checkboxes are the progress bar. This is the same shape as a workflow document: steps, outputs, guardrails, done criteria.
Phase 2: execution, one route at a time#
The loop per route:
- Give the agent the plan section for this route, the Angular files for it (read-only), and the design-system components it must use.
- Ask it to read the Angular route and restate the behaviour: data in, actions, validation, permissions, side effects.
- Build the React version against the locked design. Wire it into the routing shell.
- Port the form validation and permission checks exactly. This is where subtle bugs hide.
- Cover the empty, loading and error states from the design decisions.
- Add the tests the checklist names.
- Run the app. Do the parity check yourself, side by side with the Angular route.
- Ask the agent to tick the plan checkboxes and write the changelog entry as part of the step.
- Review the diff like any PR. Confirm it touched only the React app.
- Commit. Flip the route flag so this path serves React. Leave the Angular route in place, dark, for a rollback window.
Prompting a step
Keep the prompt scoped. Something like: "Here is the plan section for /invoices and the Angular files that implement it. The Angular code is read-only reference. Build the React route in apps/web/ using the components in packages/ui/. Match the query-param behaviour. When the code is done, update the checkboxes in plan.md and add a changelog entry in the project format. Show me a diff limited to apps/web/."
Parity and cutover#
Parity check
Same data, same actions available, same validation messages, same permission behaviour, plus the UX improvements that were deliberately planned. Anything different on purpose goes in the changelog so QA and support are not surprised.
Cutover
A route table decides which app serves each path. A reverse proxy, or a top-level router that mounts React for migrated paths and falls through to Angular for the rest. A per-route flag lets you move one path at a time and move it back instantly.
// route-table.ts: single source of truth for who serves what
export const MIGRATED = new Set<string>([
"/login",
"/invoices",
// "/invoices/:id", <- next
])
// edge / proxy: if the path prefix is migrated, send to the React app,
// otherwise send to the Angular app. One line to roll a route back.
export const target = (path: string) =>
[...MIGRATED].some((p) => path === p || path.startsWith(p + "/"))
? "react"
: "angular"Keep a changelog#
Every route that moves gets an entry: what was migrated, any behaviour that changed on purpose, and how to roll it back (flip the flag, remove the path from the set). The changelog plus the plan's checkbox state is the migration's memory. It is how a new person, or the agent after a context reset, picks up exactly where the last session stopped.
What the agent is good and bad at here#
| The agent is good at | You still own |
|---|---|
| Reading an unfamiliar Angular component and restating its behaviour | The design judgement and the redesign decisions |
| Scaffolding the React equivalent against a given design system | The migration order and what counts as low vs high risk |
| Porting form validation and permission checks | Deciding parity vs a deliberate change |
| Translating RxJS streams to hooks for the common cases | Catching the subtly wrong translation in a complex stream |
| Writing the tests and the changelog entry | Anything touching auth, permissions or money |
| Keeping the plan checkboxes current | The cutover call and the rollback decision |
Common failure modes#
- Starting Phase 2 before the shared foundation exists, so every route reinvents auth and layout.
- Letting the agent "improve" a flow mid-migration that was not in the design decisions.
- Migrating the hardest route first because it is the most interesting.
- Removing the Angular route the same day, with no rollback window.
- A vague guardrail, so the agent edits an Angular file to "make the migration easier".
- No changelog, so a paused migration is impossible to resume without re-reading every diff.