Context
Our logistics platform’s customer-facing flows—booking, tracking, documentation—ran on a classic PHP + jQuery stack that had served well for years but was buckling under new demands. Server-rendered pages with scattered jQuery for interactivity meant every non-trivial change touched PHP templates, inline scripts, and database calls with no clean seams.
The pain crystallized when we tried building richer, more interactive experiences: real-time form feedback, conditional flows, live validation. Patching those into the old system produced code that was slow to write, impossible to test reliably, and painful to maintain.
I led the team through an incremental migration to React on the frontend and focused Node.js services on the backend. This wasn’t a rewrite for its own sake; it was about restoring developer velocity and making interactive features affordable again.
(For the broader PHP + TypeScript coexistence story, see scaling mixed php + typescript systems in logistics .)
Problem
The legacy setup compounded several costs:
- Business logic smeared across PHP views, jQuery handlers, and raw SQL—no separation of concerns
- Almost no unit-testable code; validation relied heavily on manual QA
- Small UI changes forced engineers to reason about the entire PHP request lifecycle plus client state
- Hiring and onboarding slowed because modern JavaScript patterns were rare in the codebase
The hidden tax was opportunity: features we wanted to build were simply too expensive in time and risk.
Constraints
We had to modernize without breaking the business:
- Zero allowable downtime—customers used these flows daily
- Data integrity preserved at every step
- Limited team experience with React/Node; I’d be ramping people while shipping
- Stakeholder skepticism about “heavy JavaScript” SPAs from past painful experiences
The decisive constraint: no big-bang rewrite. Every phase had to deliver working value and allow instant rollback.
What I Changed
I sliced the migration by business workflow rather than tech layer. Each major flow became its own incremental target.
Backend shift
Replaced sprawling PHP endpoints with small, focused Node.js services exposing clean REST APIs. Consistent error shapes, request validation (Joi + TypeScript), typed responses. Retained the existing database layer early on (avoiding dual writes), but wrapped access in testable repository-style patterns.
Frontend re-architecture
Rebuilt each flow in React with:
- Composable components and hooks tailored to logistics forms (e.g., reusable
<ShipmentAddressForm />,<TrackingTimeline />) - Domain-specific state machines for multi-step flows with optimistic updates + server rollback
- Parallel client- and server-side validation (Zod schemas shared via TypeScript)
- Progressive enhancement so basic submission still worked with JS disabled
Integration contracts
Defined API shapes with TypeScript interfaces shared between frontend and backend. This turned integration bugs into compile-time errors instead of production surprises.
Safe rollout
Feature flags + route-based coexistence. New React flows launched at /new/booking, then gradually took over primary routes after validation. Parallel operation allowed quick reversion if anything went sideways.
Validation
- Functional equivalence — exhaustive mapping of legacy behaviors (including edge cases: partial submits, network flakes, concurrent edits) and manual + automated checks
- Performance — faster TTI, snappier interactions, shift from full-page reloads to targeted API calls
- Quality signal — frontend bug reports dropped roughly 50% in the two quarters post-migration (directional; other process changes contributed)
- Team velocity — features that once spanned weeks now routinely shipped in days, all within a single-language ecosystem
Outcome
After migration, frontend bug reports dropped roughly 50% over the next two quarters (directional), while the team could ship interactive features in days instead of weeks.
New engineers became productive far faster. Testing became realistic. The work laid groundwork for later improvements (dashboard perf, real-time tracking, etc.).
Most importantly, it proved incremental modernization could coexist with active product development—building trust for future technical bets.
Tradeoffs & Lessons
Real costs included:
- Larger initial bundle size (mitigated with code-splitting and lazy-loading, but still a tradeoff vs. jQuery)
- New runtime to operate (Node services → added observability, logging, health checks)
- Team ramp-up time (offset by pairing, starting with lower-risk flows, and living documentation)
Core lesson: migrations win when anchored in business continuity and user outcomes, not tech purity. The new stack was a tool to unlock faster, more reliable iteration.
(This pattern recurs in incremental modernization vs big-bang rewrite .)
What I’d Do Differently Next Time
- Capture baselines formally upfront — cycle time per story, bug arrival rate, deployment frequency — so improvements are hard numbers, not trends.
- Invest earlier in a shared testing harness (unit + integration) before the first component landed.
- Write ADRs and inline docs during implementation instead of retroactively.
This experience also became the seed for the legacy module deprecation checklist I still reference.
Incremental progress, measurability, and reversibility remain my default stance when business continuity is non-negotiable.