NextjsMigrating Pages Router to App Router

Migrating Pages Router to App Router

Next.js was designed so the Pages Router (pages/) and the App Router (app/) can coexist in the same project. The App Router takes priority for any route it defines, so you can migrate one route at a time, ship each step, and keep the rest of the app running on the Pages Router until it too is converted. Nothing forces a big-bang rewrite.
Recommended migration order

Step

What to do

  1. Set up app/

Create the app/ directory alongside pages/ and add a root app/layout.tsx containing <html> and <body> — required before any App Router route can render

  1. Migrate shared layout first

Port the chrome that wraps every page (nav, footer, providers) from pages/_app.tsx / _document.tsx into app/layout.tsx

  1. Move leaf pages one at a time

Convert individual routes — start with simple, low-traffic pages — into app/<route>/page.tsx, deleting the old pages/<route>.tsx once it works

  1. Convert data fetching

Replace getServerSideProps/getStaticProps with async Server Components that fetch or query directly

  1. Replace _app.tsx patterns

Global CSS imports, context providers, and theme setup move from _app.tsx into the root layout (or nested layouts for section-specific providers)

  1. Remove pages/ once empty

Once every route has an App Router equivalent, delete the Pages Router files entirely

Data fetching: before and after

pages/posts/[id].tsx — Pages Router

TSX
export async function getServerSideProps({ params }) {
  const res = await fetch(`https://api.example.com/posts/${params.id}`)
  const post = await res.json()
  return { props: { post } }
}

export default function PostPage({ post }) {
  return <article>{post.title}</article>
}

app/posts/[id]/page.tsx — App Router

TSX
export default async function PostPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const res = await fetch(`https://api.example.com/posts/${id}`)
  const post = await res.json()

  return <article>{post.title}</article>
}
The Server Component version fetches data directly inside the component instead of through a special exported function, and the result is streamed as part of the initial render rather than passed in through a separate props shape.
Common friction points
useRouter has moved
useRouter from next/router (Pages Router) and useRouter from next/navigation (App Router) are different hooks with different return shapes. The App Router version drops properties like pathname as a string field and query in favor of separate hooks (usePathname, useSearchParams, useParams). Importing from the wrong module is one of the most common migration errors, and it fails at build/runtime rather than being caught by a simple find-and-replace.
  • CSS-in-JS libraries (styled-components, Emotion) need an explicit registry/provider set up in the root layout to work with Server Components streaming — check the library’s App Router migration guide.

  • useRouter import path — swap next/router for next/navigation, and split usage across usePathname/useSearchParams/useParams as needed.

  • _app.tsx and _document.tsx have no direct equivalent — their responsibilities split across the root layout.tsx (HTML shell, providers, global CSS) and per-route metadata/generateMetadata (what _document.tsx’s <Head> used to manage).

  • API routes (pages/api/*.ts) become Route Handlers (app/api/*/route.ts) — the function signature changes from (req, res) to exported GET/POST functions using the Web Request/Response APIs.

Tip
Migrate route-by-route, not file-type-by-file-type. Pick one low-risk page, take it all the way through — layout, data fetching, client interactivity — and ship it before starting the next one. This keeps the app releasable at every step and makes it much easier to isolate which change caused a regression, if any.
Note
The two routers can run side by side indefinitely if needed — there is no deadline imposed by the framework. Most teams do eventually finish the migration, since new Next.js capabilities land in the App Router first, but the incremental path means you never need to block a release to get there.