Selected work · Sanitized case study

Migrating a Large Frontend Without Freezing Product Development

The difficult part of a frontend migration is rarely changing an import. It is creating a sequence in which the architecture can change while product teams continue to ship.

60+application routes
40files in a representative migration slice
23route modules changed in that slice
16 → 3framework packages
−591net lines of code
≈30%smaller bundle in the measured slice

01 · Context

A working product had outgrown its framework boundary.

The application had more than 60 routes and the usual web of loaders, actions, authentication, analytics, test helpers, server entry points, and deployment tooling around them. The existing stack still worked. That made the migration harder to prioritize, not less important: every new feature increased the surface area that would eventually have to move.

02 · Organizational problem

Product work always won the backlog fight.

A rewrite would have asked the organization to stop earning value while engineering rebuilt capabilities users already had. Leaving the system alone carried a quieter cost: duplicated framework packages, manual route types, aging conventions, and a growing difference between how new and existing code worked.

The real problem was not persuading people that migration had value. It was designing the work so that feature delivery did not depend on the migration finishing first.

03 · My responsibility

Own the path, not every pull request.

My responsibility was to turn a framework change into a sequence other engineers could safely follow: establish the target architecture, identify the compatibility boundary, migrate a representative vertical slice, document the failure modes, and make validation cheap enough to run continuously.

That meant treating developer experience as part of the architecture. A theoretically clean destination was not useful if every route required bespoke knowledge to reach it.

04 · Constraints

The migration had to remain subordinate to the product.

  • Existing routes and user-visible behavior had to remain stable.
  • Feature work needed to merge throughout the migration.
  • Authentication, analytics, and deployment behavior crossed route boundaries.
  • TypeScript exposed interface drift that the old framework conventions had hidden.
  • The team could not support two permanent architectures.

Team count, incident data, and employer-specific delivery dates are not included here. The public measurements come from a representative migration slice whose code history I could inspect directly.

05 · Architecture decision

Choose continuity over novelty.

I chose React Router framework mode as the destination because it preserved the route-module model—loaders, actions, forms, links, and server rendering—while collapsing the framework dependency surface. The representative slice moved from 16 framework packages to 3 and replaced manually maintained loader interfaces with types generated from the actual route implementation.

The important decision was the boundary: shared entry points and configuration moved first, while route behavior stayed recognizable. This made the compiler a migration queue instead of turning production into the test environment.

06 · Migration strategy

Prove the lane, then move traffic through it.

  1. Inventory the cross-cutting surface. Map packages, entry points, route conventions, authentication, tests, and deployment assumptions before editing product code.
  2. Change the foundation once. Replace framework packages, build commands, Vite integration, and generated route types in a controlled slice.
  3. Migrate in dependency order. Entry points came first, followed by 23 route modules, 8 components, and 7 utilities in the measured slice.
  4. Let types expose hidden coupling. Resolve loader and component contracts rather than masking errors with compatibility types.
  5. Keep each step releasable. Build, type-check, and exercise routing after each coherent batch so product work could continue to land.
  6. Record the traps. File-extension rules, splat-route naming, case-sensitive queries, and server entry behavior became reusable migration guidance.

07 · Result

Less framework, without a frozen roadmap.

The representative slice took two focused sessions and touched 40 files: 327 insertions and 918 deletions, a net reduction of 591 lines. The framework dependency set fell from 16 packages to 3, route types became generated artifacts rather than duplicated interfaces, and the measured bundle was roughly 30% smaller.

The larger result was operational: the application retained its route model and product teams did not have to relearn the system all at once. The migration removed a duplicated framework layer instead of introducing another permanent abstraction.

Read the measured migration notes or explore the broader frontend migration series.

08 · What I would change

Measure the baseline before the first dependency changes.

I would capture route-level bundle, build-time, and deployment measurements before touching configuration. The code history preserved file counts and line changes; performance evidence was less systematic. I would also build the route inventory earlier and classify routes by shared infrastructure, because the first few migrations revealed coupling that naming conventions alone did not show.

I would keep the incremental strategy. A migration earns trust when each step reduces uncertainty and leaves the product in a state the next engineer can understand.