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
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.
Modern CSS in 2026: Container Queries, :has(), and Tailwind v4
Container queries, :has(), cascade layers, color-mix() — the CSS features I'm actually using on production projects in 2026 and how they change the way I structure components.
How to Hire a Blockchain Developer in Pakistan in 2026
A practical guide to hiring blockchain developers from Pakistan in 2026 — where to find them, what rates to expect, red flags, contract structure, and how to evaluate Solidity skill in 30 minutes.