Next.js App Router vs Pages Router: Lessons From 12 Project Migrations
What changes, what breaks, what to migrate first, and what to leave alone. Hard-earned migration lessons from moving 12 client projects from Pages Router to App Router.
When to migrate (and when not to)
I've migrated 12 production Next.js apps from Pages Router to App Router in the last 18 months. Some migrations took 3 days. One took 6 weeks. The difference wasn't app size — it was complexity in three specific areas: authentication, data-fetching patterns, and dynamic imports.
Migrate when:
Don't migrate when:
Migration playbook
Step 1: Run them in parallel
You don't have to migrate all-at-once. App Router and Pages Router coexist in the same project. Move new routes to app/, keep old routes in pages/, ship continuously.
This single decision turns a "scary 6-week migration" into "incrementally migrate over 3 months."
Step 2: Move the layout first
pages/_app.js and pages/_document.js map to app/layout.js. This is your first migration win — your global providers, fonts, and metadata move once.
Step 3: Migrate the simplest leaf pages first
Pick a static or near-static page. Maybe a privacy policy, or an about page. Verify in the browser. Ship.
Step 4: Migrate data-fetching pages
This is where it gets real. Old: getServerSideProps + props. New: async function in the page component itself.
Catch: fetch in App Router is cached aggressively by default. If you were using getServerSideProps to bust the cache on every request, you need { cache: "no-store" } or export const dynamic = "force-dynamic".
Step 5: Migrate API routes (mostly)
pages/api/users.js → app/api/users/route.js. The Web standards (Request, Response) replace Express-style req/res. Most things translate; some middleware patterns need rewriting.
Step 6: Authentication is its own project
If you're on NextAuth, upgrade to v5 (Auth.js). v5's App Router support is solid; v4's was painful. If using Clerk, their App Router SDK is the cleanest of any auth library I've used.
Step 7: Dynamic imports
dynamic() only works inside client components in App Router. If you need to lazy-load a component from a server component, wrap it in a client component first.
What breaks
Things that broke for me on at least one of the 12 migrations:
_app.js logic that ran on every page (analytics, error boundaries) — needs to move into a client component wrapper inside layout.jsnext/router — replaced by next/navigation (useRouter, usePathname, useSearchParams)fill mode requires a relative-positioned parentWhat surprised me
Should you migrate today?
If you're on Next.js 14.x and Pages Router:
When clients hire me for this
A typical Pages → App Router migration engagement is 2-4 weeks for a medium SaaS app. Deliverable is a fully-migrated app with the test suite passing, deployment pipeline updated, and a runbook for the team.
If you have a Next.js project on Pages Router and want a senior to lead the migration, contact me. React + Next.js work is part of my MERN engagement track alongside Web3.
You might also like
Real-Time Apps with Next.js Server Actions and WebSockets in 2026
When Server Actions are enough, when you need a WebSocket layer, and how to wire Pusher / Soketi / Ably into a Next.js 14 App Router project without breaking SSR.
React Server Components in Next.js 14: Production Patterns That Work
When to use Server Components vs Client Components in Next.js 14, the patterns that survive production, and the foot-guns I keep tripping over after shipping 8+ App Router projects.
TypeScript Generics for React Engineers: A Practical Guide
The 6 generic patterns I use weekly on React + Next.js codebases — typed hooks, polymorphic components, discriminated unions, infer, constraints — without the academic noise.