# Next.js App Router Migration: Lessons from a Real Project

> Hard-won lessons from migrating a production Next.js app from Pages Router to App Router — what breaks, what shines, and what I'd do differently.

URL: https://www.matthewswong.com/en/blog/nextjs-app-router-migration-lessons/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2025-06-10
- Updated: 2026-10-17
- Category: Web Dev

By mid-2025, Next.js 15 had made the App Router the clear default for new projects — 80% of new Next.js projects start with it. But what about the hundreds of thousands of existing Pages Router apps? Migrating a production web app from Pages Router to App Router can be one of the most educational (and occasionally painful) experiences a developer has. This is what to know before you start.

## Why Migrate at All?

Pages Router isn't going away tomorrow — Vercel still maintains it and it still ships new features. But App Router unlocks capabilities that Pages Router fundamentally cannot offer: React Server Components (zero client-side JS for data-fetching components), Streaming with Suspense, parallel routes, intercepting routes, and the new caching model. On our commercial app, the main motivation was performance — Server Components let us move significant data-fetching logic off the client, reducing Time to Interactive (TTI) measurably.

### The Mental Model Shift

The biggest hurdle isn't syntax — it's thinking in two rendering environments simultaneously. In Pages Router, everything is a React component and data fetching happens in getServerSideProps or getStaticProps at the page level. In App Router, every component defaults to being a Server Component. The moment you add useState, useEffect, or any browser API, you need to mark it 'use client'. This boundary is not just a directive — it's an architectural decision about where state lives.

> From my migration experience: start by auditing every component for useState, useEffect, and event handlers before you touch a single file. Color-code them — green for pure render (can stay server), yellow for data hooks only, red for browser interaction. This audit saves you from 'use client' sprawl where you mark entire subtrees as client when only a leaf component needs it.

## What Actually Breaks During Migration

The official Next.js migration guide is good but undersells the real friction. In practice, the hardest parts were: (1) Libraries that assume browser globals — chart libraries, rich text editors, date pickers that access window or document directly will throw on the server. You need dynamic() with ssr: false for these. (2) useRouter from next/router is completely different from useRouter in next/navigation — same name, different behavior. The App Router's useRouter cannot read query params; you need useSearchParams() separately. (3) API routes move from pages/api/ to app/api/[...route]/route.ts with a completely different export signature.

### Handling the Data Fetching Transition

getServerSideProps and getStaticProps don't exist in App Router. Server Components fetch data directly — you just await fetch() or call Prisma at the top of the component. This is simpler in theory but requires understanding Next.js's automatic caching. By default, fetch() in Server Components is cached. If you need fresh data on every request, you must add '{ cache: ''no-store'' }'. If you need revalidation, use '{ next: { revalidate: 60 }' '}'. Miss this and you'll spend hours debugging why your data is stale.

> **The Middleware Authorization Bypass (CVE-2025-29927)**

> Critical: Never rely on Next.js middleware as your sole authorization gate. CVE-2025-29927 exposed that middleware could be bypassed by manipulating the x-middleware-subrequest header. Your actual authorization logic must live in Server Components or API routes — middleware is for routing decisions and locale detection, not security enforcement. Always verify session/permissions server-side inside the route handler itself.

## The Incremental Migration Strategy

You don't have to rewrite everything at once. Next.js supports running Pages Router and App Router side-by-side during migration. WorkOS's approach — which I found invaluable — is to test the entire App Router version behind a feature flag before switching production traffic. We started with the least interactive pages (marketing landing pages, blog), converted those to Server Components, validated, then moved to authenticated dashboard pages. The dashboard was hardest because it's all client state: tables with sorting, forms with validation, real-time status updates.

## What I Use and Why

Today, new projects should start with App Router. The benefits compound: Streaming means users see content progressively rather than waiting for the slowest data fetch. Server Components mean the Prisma client never gets shipped to the browser. Nested layouts mean shared state (like a user session) flows down without prop drilling. For brownfield migrations, do it incrementally — Pages Router stays for complex interactive sections, App Router takes over static and SSR pages first. The key insight: App Router is not harder, it just requires understanding a new mental model. Once it clicks, it's genuinely better.

## Migration Checklist

Before starting any Pages Router to App Router migration: (1) Audit all third-party libraries for SSR compatibility — check their issues for 'use client' or 'window is not defined' reports. (2) Map all getServerSideProps and getStaticProps usages to their App Router equivalents. (3) Identify all pages that use useRouter for query params — they'll need useSearchParams. (4) Plan the layout hierarchy — root layout, locale layouts, dashboard layouts — before writing code. (5) Set up the App Router under /app alongside your existing /pages to run both simultaneously during migration. (6) Never put authorization logic only in middleware — always enforce it in the route handler.

## Sources & Further Reading

- Next.js Docs — App Router Migration Guide — https://nextjs.org/docs/app/guides/migrating/app-router-migration
- WorkOS Blog — Migrating to Next.js App Router with Zero Downtime — https://workos.com/blog/migrating-to-next-js-app-router-with-zero-downtime
- ProjectDiscovery — CVE-2025-29927 Next.js Middleware Bypass — https://projectdiscovery.io/blog/nextjs-middleware-authorization-bypass

## Source links

- https://nextjs.org/docs/app/guides/migrating/app-router-migration
- https://workos.com/blog/migrating-to-next-js-app-router-with-zero-downtime
- https://projectdiscovery.io/blog/nextjs-middleware-authorization-bypass

## FAQ

### What is the biggest mental model shift when migrating from Pages Router to App Router?

The core shift is thinking in two rendering environments simultaneously. In Pages Router, every component is a React component and data fetching happens in getServerSideProps or getStaticProps at the page level. In App Router, every component defaults to being a Server Component, and you must explicitly mark components with 'use client' the moment you add useState, useEffect, or any browser API.

### How should I approach the 'use client' boundary to avoid sprawl?

Before touching any files, audit every component for useState, useEffect, and event handlers. Color-code them — green for pure render (safe as Server Component), yellow for data hooks only, red for browser interaction. This prevents the common mistake of marking entire component subtrees as client when only a single leaf component actually needs browser access.

### What are the most common breakages during a Pages Router to App Router migration?

Three issues cause the most friction: first, third-party libraries that access window or document directly (chart libraries, rich text editors, date pickers) throw on the server and require dynamic() with ssr: false. Second, useRouter from next/router and useRouter from next/navigation share the same name but different behavior — the App Router version cannot read query params, requiring useSearchParams() separately. Third, API routes move from pages/api/ to app/api/[...route]/route.ts with a completely different export signature.

### Why is it dangerous to put authorization logic only in Next.js middleware?

CVE-2025-29927 exposed that Next.js middleware can be bypassed by manipulating the x-middleware-subrequest header. Middleware should only handle routing decisions and locale detection. Actual session and permission checks must always be enforced server-side inside the route handler or Server Component itself.

### What is the recommended strategy for migrating an existing production app incrementally?

Next.js supports running Pages Router and App Router side-by-side, so you do not have to rewrite everything at once. Start with the least interactive pages such as marketing landing pages and blog posts, convert those to Server Components, validate, then move to authenticated dashboard pages. Testing the full App Router version behind a feature flag before switching production traffic — an approach documented by WorkOS — helps ensure zero-downtime migration.

---

- HTML version: https://www.matthewswong.com/en/blog/nextjs-app-router-migration-lessons/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/nextjs-app-router-migration-lessons/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
