
Moving from Pages Router to App Router isn't a refactor. It's a different way of thinking about your entire application. The technical parts were the easy part.
We migrated a mid-sized Next.js application from Pages Router to App Router over about three months. The codebase had roughly forty routes, a handful of API routes, a custom authentication setup, and enough legacy patterns that the migration wasn't going to be a weekend project. I went in expecting the technical challenges: rethinking data fetching, converting components, dealing with layout changes. What I didn't expect was how much of the difficulty was conceptual rather than mechanical.
The App Router works. It's genuinely better for the use cases it was designed for. But the gap between understanding how it works in isolation and understanding how it works in a real application with real requirements took much longer to close than I anticipated.
The Pages Router is a routing convention. The App Router is a rendering architecture. That difference matters more than it sounds.
The mental model shift is the hard part
In the Pages Router, the default is client-side. Everything is a React component that runs in the browser unless you explicitly opt into server-side rendering with getServerSideProps or static generation with getStaticProps. The App Router flips that: the default is server. You add "use client" when you need browser APIs, state, or interactivity.
This seems simple, but the implications ripple through everything. Every component you've ever written assumed it runs on the client. Every third-party library you use probably makes that assumption too. Moving to a server-first model means auditing everything — not just your own code, but the assumptions baked into your dependencies.
- UI libraries that use
useContextoruseStateinternally need client boundary wrappers - Authentication logic that lived in
_app.tsxneeds rethinking in a server-first layout - The
routerobject fromnext/routerdoesn't exist in Server Components - Cookie and header access now happens in Server Components, not API routes in many cases
Data fetching: simpler and more confusing at the same time
The old pattern — getServerSideProps at the page level, props drilled down to components — was verbose. But it was explicit. You always knew where data came from, and the data boundary was always at the page level.
With the App Router, any Server Component can fetch data directly. That's genuinely useful — the data lives next to the component that uses it, no prop drilling. But it also means the data fetching is now distributed across the component tree, and understanding what gets fetched where requires reading the whole tree, not just the page file.
The caching model will humble you
The App Router has its own caching layer that sits between your application and the network. It's powerful and it's the reason App Router pages can be so fast. It's also the most common source of confusion I encountered during the migration.
The default behavior caches aggressively. A fetch() in a Server Component is cached at the network level by default. Data that should be fresh is sometimes stale. Pages that should revalidate don't, or revalidate too often. Getting this right required reading the caching documentation carefully — not the quick-start guide, but the full specification — and testing the actual behavior rather than inferring it.
My recommendation: spend time on the caching docs before you start, not after things start behaving unexpectedly. The mental model for caching in the App Router is coherent once you understand it, but there are enough layers (fetch cache, Data Cache, Full Route Cache, Router Cache) that the behavior can seem arbitrary if you're discovering it through trial and error.
What I'd tell myself at the start
Don't migrate everything at once. The App Router supports incremental adoption — you can have Pages Router routes and App Router routes coexisting in the same application. Start with new features in the App Router. Learn the mental model without the pressure of a broken production route. Then migrate incrementally, starting with the simpler routes that don't have complex data requirements or heavy client-side interactivity.
The migration is worth doing. The App Router enables patterns — streaming, parallel data loading, server-side mutations with Server Actions — that aren't possible in the Pages Router. But give it time and don't underestimate the learning curve. It's not a bigger Pages Router. It's a different thing.