Frontend architecture · Incremental migration · 2026 · measured June → September

Leading a gradual Razor-to-React migration without stopping a live product

The product was a live multi-tenant ASP.NET Core platform whose interface had grown across MVC, Razor, JavaScript/jQuery and some Blazor components. A rewrite was not realistic, so I owned the frontend direction of an incremental React + TypeScript migration: hybrid navigation, feature-owned routes, a same-origin BFF, a reusable page shell and build safeguards, so the legacy interface and React could coexist until each business area was safe to replace.

My role
Frontend architecture and implementation direction: mine. Business features on the platform: the whole team.
Stack
  • React
  • TypeScript
  • React Router
  • Redux Toolkit
  • RTK Query
  • Vite (library mode)
  • ASP.NET Core
  • BFF

At a glance

  • Real React routes grew from 10 to 112 between the June baseline and the September snapshot
  • Entry JavaScript artifact 1,764,861 → 63,249 bytes (the entry chunk only)
  • 108 real routes on the staging line, 65 of them explicitly mapped to legacy URLs

Context

The product was a live, multi-tenant ASP.NET Core platform whose interface had grown across MVC, Razor, JavaScript/jQuery and selected Blazor components. A full rewrite was not realistic: customers depended on the existing application, feature development had to continue, and many legacy areas were coupled to authentication, tenant context and operational workflows.

The constraints were concrete. The existing product had to stay usable throughout. Legacy and React navigation had to coexist without breaking authorization or deep links. The browser could never receive the downstream API bearer token. New features needed repeatable patterns for state, API contracts, layout and error handling. And every change went through testing and staging before production, so it had to be compatible and rollback-friendly.

What I did

  • Mounted React and TypeScript under /app inside the existing ASP.NET host, with React Router using /app as its basename while ASP.NET kept session handling and server-side integration.
  • Set up the browser → same-origin BFF flow: the API JWT stays in the server-side session, React receives an authentication snapshot and uses CSRF protection for mutations. Redux Toolkit and RTK Query became the shared model for server state, caching and per-feature endpoint injection.
  • Designed the hybrid navigation model — legacy page, automatic resolution or explicit React route — with shared utilities that preserve query strings and hashes and handle SPA, full-page and external destinations through one interface.
  • Moved route discovery to feature-owned definitions via import.meta.glob, with duplicate-route checks, so a normal feature no longer edits a central registry.
  • Built PageShell, a reusable layout with a header, an optional subheader and a scrollable body: desktop operational pages keep actions and filters visible while the data area scrolls; small screens fall back to natural page scrolling.
  • Implemented route and vendor code splitting in the Vite library-mode build while preserving the ASP.NET bridge, kept a single stylesheet (split CSS could be orphaned in this setup) and added a bundle verifier that fails the build on missing referenced chunks.

Decisions

Incremental, inside the same host

No second application and no big-bang cutover. React lives under /app in the existing ASP.NET host, so each business area moves when it is safe and the legacy page stays available until then.

The token stays on the server

The BFF keeps the API JWT in the server-side session; the browser only ever gets an authentication snapshot and a CSRF token for mutations.

Routes owned by features

Route definitions live next to the feature and are discovered at build time. The central registry had become a merge hotspot and an easy place to forget an entry.

Split JavaScript, one stylesheet

Code splitting for routes and vendors, but a unified CSS bundle: in library mode a split stylesheet could end up never loaded. The verifier catches a missing chunk at release time instead of in production.

The hard part

As the React surface grew, the BFF and the routing became the main source of integration risk: a large central endpoint surface, dependency registration that could diverge between production and tests, shared HTTP headers and client/server contract mismatches on one side; duplicated resolvers and inconsistent destinations on the other.

Stabilizing that meant per-feature endpoints, services and DTOs with shared dependency-injection composition, per-request HTTP headers, client/server inventory checks, Swagger contract verification, centralized navigation and release-time bundle checks. I worked with the person promoting release branches and stayed involved through staging until frontend failures reached a stable operational level.

Result

  • Between the verified June baseline and the September snapshot, real React routes grew from 10 to 112 at HEAD; the examined staging line held 108 real routes, excluding 42 placeholders and two development-only routes.
  • Of the 108 staging routes, 65 were explicitly mapped to legacy URLs, 16 were declared native React routes and 27 were additional detail, create, edit or otherwise unmapped paths.
  • The React pages promoted to staging were operational according to direct engineering observation; the repository held 317 frontend test files and the migration tracker passed 16 of 16 tests.
  • The durable outcome is a migration platform: feature-owned routes, shared navigation, a reusable page structure, BFF boundaries, consistent state patterns and release safeguards that let React work expand while the legacy product stayed available.

Numbers, with their limits

  • Real React routes at HEADBefore10After112June baseline → September 2026 snapshot
  • Real routes on the staging line108excluding 42 placeholders and 2 development-only routes
  • Staging routes mapped to a legacy URL65 of 10816 native React routes, 27 detail/create/edit or unmapped paths
  • Entry JavaScript artifactBefore1,764,861 bytesAfter63,249 bytesthe entry chunk only, not total JavaScript
  • Frontend test files · migration tracker317 files · 16 / 16 testshistorical counts, September 2026

What these numbers do not say

  • A route is not a complete business feature, and route counts say nothing about how much of the legacy product has been migrated.
  • The entry-chunk reduction is not a reduction of total transferred JavaScript or of page-load time; neither was measured.
  • Operation in staging is an engineering observation, not a route-by-route smoke test, customer activation or production adoption. Those were not retained as verifiable metrics, so I do not present this as a completed migration.

What it proves

I can design a migration path that lets a new stack grow inside a live legacy product — with security boundaries, repeatable patterns and release safeguards — and stay accountable through stabilization.

Attribution

I owned the frontend architecture and implementation direction, using AI assistance for repository analysis and implementation while keeping the decisions, the review and the release readiness. Business features built on the platform belong to the whole team, and the UI-library choices were shared decisions.