Migrating Pages Router to App 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 |
|---|---|
| Create the |
| Port the chrome that wraps every page (nav, footer, providers) from |
| Convert individual routes — start with simple, low-traffic pages — into |
| Replace |
| Global CSS imports, context providers, and theme setup move from |
| 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
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
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>
}props shape.Common friction points
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.
useRouterimport path — swapnext/routerfornext/navigation, and split usage acrossusePathname/useSearchParams/useParamsas needed._app.tsxand_document.tsxhave no direct equivalent — their responsibilities split across the rootlayout.tsx(HTML shell, providers, global CSS) and per-routemetadata/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 exportedGET/POSTfunctions using the WebRequest/ResponseAPIs.