Legacy frontend modernization
No TypeScript. No linter. No Sentry. 170 locations use it daily.
Modernizing an untyped, unmonitored legacy frontend incrementally while keeping the product available to existing users.
5 min readBy Abdullah Raheel
The inherited constraints were real
The inherited CRA frontend was vanilla JavaScript with no TypeScript, ESLint, Prettier, Sentry, reusable component system, or coherent feature structure. It contained more than 5,000 lines of raw CSS, Tailwind classes overridden by inline styles, copied pages, and a flat directory layout.
The problems reinforced each other. API changes reached the browser without compiler feedback. Copied screens repeated the same styling bugs. Production errors usually arrived as user reports because there was no monitoring path.
The maintenance cost reached users
The application still served 170 customer locations every day. We did not convert V1 in place or wait for a complete replacement. We kept the old routes running and required every new V2 route to meet the new standards.
That gave the team a simple rule: do not expand the legacy conventions. Move ownership when a route is being rebuilt, then remove the old path after the new one is operating in production.
Create a typed perimeter
Next.js 15 and TypeScript formed the new perimeter. New routes used typed route definitions, generated API clients, shared fetch behavior, and feature-owned components. The team did not first convert every legacy file; it stopped adding new untyped surface area and moved ownership into V2 as work reached each route.
- TypeScript and typed routes made navigation and API-shape mistakes visible during development.
- Generated clients and shared fetch behavior replaced scattered request conventions.
- Feature folders gave routes, hooks, schemas, and tests explicit owners.
- Reusable primitives replaced page-level copies for repeated interaction patterns.
Add feedback loops before moving every screen
Modernization also changed how failures were detected and how changes were admitted. Sentry added client, server, and edge reporting plus application error boundaries. Unit and browser workflows gained explicit required gates, while behavior-focused tests covered high-risk flows such as Investor Intake.
These controls did not repair V1 automatically. They made each V2 route observable and testable before it inherited more production responsibility.
Build the new perimeter around the old product
V2 owned global chrome, authentication, and each newly migrated feature. CRA stayed inside one iframe only for routes it still owned. The companion migration article covers the postMessage, SSO, and routing bridge in detail.
For this article, the important point is ownership: new standards applied at the V2 boundary, and every completed migration reduced the amount of code exempt from them.
Make ownership visible during the transition
Ownership was tracked explicitly during the transition. Navbar, Topbar, Settings, Investor Intake, Authentication, and Twilio moved to V2. Home, Genius Mode, Deals, and Buyers stayed in V1 until their replacements were ready. Customer locations moved in controlled groups rather than receiving every unfinished surface at once.
That inventory prevented the rewrite from becoming an undefined parallel project. A feature was either still owned by V1, served through the compatibility boundary, or native in V2.
Modernization is a sequence, not an event
The final cutover removed the iframe, legacy generated clients, and the bridge code, then cleaned obsolete routes, tests, auth guards, dialer sharing, and component structure. Modernization was complete only when the temporary boundary could be deleted.
- Do not measure progress by the number of newly created framework files.
- Measure which production responsibilities have moved and which compatibility paths can be removed.
- Keep new code inside the new standards instead of waiting for a perfect whole-codebase conversion.
- Delete temporary migration infrastructure after the last dependent surface moves.

