A 500,000-line monorepo does not migrate over a weekend. Ours took two quarters, ran in production throughout, and never required a flag day.
Route-by-route, behind the same domain
Next.js will serve the Pages Router and the App Router simultaneously. That single fact is what made an incremental migration possible: we moved one route group at a time, shipped it, watched the metrics, and moved on.
The invalidation traps
Data fetching semantics changed more than the file layout did. The App Router caches aggressively by default, and several of our dashboards silently served stale data for a week before we caught it in a support ticket rather than a dashboard.
- Audit every fetch for its caching intent before you move the route.
- Server Components cannot hold client state — split the boundary deliberately.
- Shared layouts re-render less than you expect; do not rely on remount side effects.
Boundaries we drew on purpose
The migration forced a question we had been avoiding: which parts of the app genuinely need interactivity? The answer was less than a third, and the bundle size reduction from that honest accounting outweighed everything else the migration delivered.
