
Large Next.js migrations are rarely hard because of the framework. They are hard because the system still has to serve users, ship features, and support the business while you change how it is built.
I do not rewrite. I build a path from the system you have to the system you want, with both usable along the way.
That sounds obvious until a program starts optimizing for the wrong scoreboard: percent of routes moved, tickets closed in an App Router epic, a slide that says "migration complete" while product work is still stuck on the old side. Next.js can host Pages and App side by side. The framework will not stop you from treating coexistence as a rewrite with extra steps.
It is easy to begin here:
How do we move this page to the App Router?
That becomes the implementation question later. It is rarely the first one that matters.
I want the constraints around the system first:
The migration order should come from those answers. Not every app should move the same pieces first. Official guidance even assumes smaller steps: the app directory is designed to work with pages so you can migrate incrementally, page by page. The missing piece is deciding which page, and why that one, before the mechanical move.
When I run this work, I use one sequence. Framework APIs change. The sequence does not.
Caption: Migration as a loop, not a rewrite checklist. Exit when new work lands on the target side and legacy constraints are actually disappearing.
| Step | Question that gates the next move | Failure mode if you skip it |
|---|---|---|
| Map constraints | What cannot break, and what is already expensive to change? | You migrate the easy pages and leave the real risk untouched |
| Find seams | Where can architecture change without boiling the ocean? | Every change pulls half the app with it |
| Move one real path | Did production teach us auth, data, caching, deploy, observability? | A demo POC that never survives contact with the system |
| Keep dual temporary | Where should new development land this week? | Two permanent systems, neither finished |
| Prove the new path | Is the migrated slice better in production, not just cleaner in git? | A nicer repo with worse latency, errors, or JS weight |
| Score constraints removed | Can the team ship on the target architecture with less drag? | A vanity "% migrated" number that hides stalled value |
The first useful search is for boundaries that already exist. A route group that is mostly independent. A product area with its own data layer. A section with fewer ties to global state.
Those seams give you places to change architecture without asking the whole application to move at once. Sometimes the smallest meaningful unit is a route. Sometimes it is a whole section. Sometimes the first step is not moving a page at all. It is extracting a shared dependency that currently blocks everything else.
I also want a real production path through the new architecture early. Not always the most important page, and not always the easiest one. Something representative enough to expose the real problems: auth, data dependencies, analytics, error handling, caching, shared components, observability, deploy behavior.
An isolated proof of concept rarely teaches you that. The first migrated path is half implementation and half discovery. It tells you what the rest of the migration will actually require.
The Next.js team has said incremental adoption is non-negotiable: move a route, take the new capabilities at your pace, roll that route back if you need to. That is the product shape. Your job is to pick a route that teaches the organization something true.
Running old and new side by side is often necessary. Treating that temporary shape as permanent is how migrations stall.
Every compatibility layer has a cost. Every migration-only abstraction has a maintenance cost. Every duplicated implementation is another place for behavior to diverge.
There should be an obvious answer to one question:
Which side of this architecture should new development happen on?
If developers cannot answer that, you do not have a migration. You have two systems.
That is also why the migration cannot sit in a separate universe from product work. Features still need to ship. Bugs still need fixing. If every product change waits on the migration, people will eventually bypass it. Better to migrate a section as part of the next meaningful change there, or create a boundary first and let future work land behind it.
"We migrated to Next.js" is not an outcome if the product behaves the same afterward. Moving architecture is a chance to revisit old assumptions on purpose.
What needs to run on the client? What can render on the server? What can be cached? Which requests block rendering? Which dependencies dominate the bundle? Are we shipping data to the browser that never needed to leave the server?
I would rather migrate one section and make it noticeably better than migrate ten sections mechanically.
When two architectures run side by side, intuition gets less reliable. You need enough visibility to compare old and new paths: latency, errors, Core Web Vitals, cache behavior, server work, JavaScript shipped, deploy behavior. Without that feedback, it is easy to mistake a cleaner codebase for a better product. The new path can look nicer in the repo and perform worse in production.
"40% of routes migrated" is a tempting metric. It is often misleading. Twenty simple routes may matter less than one deeply connected area.
I care more about whether the migration is removing architectural constraints:
| Vanity signal | Better signal |
|---|---|
% of routes in app/ | Can new features ship on the target architecture by default? |
| Tickets closed in the migration epic | Are major legacy dependencies disappearing from the critical path? |
| "App Router adoption" slide | Are latency, errors, and JS weight better on migrated paths? |
| Lines moved | Is deployment safer and the system easier to change? |
The end goal is not a newer framework version. It is a system that is easier to change: clearer boundaries, fewer accidental dependencies, better production visibility, and a more predictable way to ship.
If the app is tiny, greenfield, or already abandoned except for a rebuild with a hard cutover date, a rewrite can be honest. Do not dress a rewrite up as incremental migration, and do not dress a stalled dual-stack up as progress.
This piece also does not prescribe your first route. Auth-heavy apps, content sites, and B2B dashboards have different seams. The sequence stays; the first cut does not.
Before the next "move page X to App Router" ticket, spend a few hours on this:
If you cannot finish that list, you are not ready to count routes. You are still discovering the system.
Next.js is part of the implementation. The migration itself is an architecture problem. So instead of asking how to rewrite the application, ask what smallest change moves the architecture in the right direction without stopping the organization around it.
I do not rewrite. I start there.
Thanks for reading.
More writing