Module P-17·30 min read

The coexistence model, page-by-page migration strategy, getServerSideProps→RSC, getStaticProps→generateStaticParams, API Routes→Route Handlers, next/router→next/navigation API differences, _app/_document→RootLayout providers, Middleware behaviour during partial migration, and the ten pitfalls that block every team mid-migration.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

P-17 — Migrating from Pages Router to App Router

Who this is for: Teams sitting on a production Pages Router codebase who need to move to the App Router without a rewrite freeze. This module assumes you know the App Router's mental model from the earlier phases — Server Components, layout.tsx, Route Handlers — and now need the mapping table and the migration order that lets you ship the move page by page, in production, without a big-bang cutover.


The Coexistence Model

The single most important fact about this migration: the pages/ directory and the app/ directory can exist in the same project at the same time, and Next.js will route both.

text

Next.js resolves routing with one rule: if a route exists in both app/ and pages/, app/ wins. In practice you won't hit that conflict on purpose — you migrate a route by moving it, not by duplicating it — but it means you can create the new app/dashboard/page.tsx and confirm it renders correctly before deleting pages/dashboard.tsx, then delete the old file once you're satisfied. There's no environment flag, no dual-build step, no adapter package. It's the same Next.js server serving both trees from one build.

This is what makes route-by-route migration realistic. You do not need to touch pages/settings.tsx to migrate pages/dashboard.tsx. Each route migrates independently, and the two directories keep running side by side for as long as your migration takes — weeks or months on a large app is completely normal.

Two things stay global for as long as anything remains in pages/:

  • pages/_app.tsx and pages/_document.tsx still run, but only for routes still under pages/. They have no effect on routes under app/.
  • app/layout.tsx is required as soon as any route lives under app/, and it governs only app/ routes.

That means during the transition you are maintaining two parallel "shells" — one via _app/_document, one via RootLayout — and any provider, global CSS import, or <html>/<body> customization has to be present in both until pages/ is fully retired.


A Sane Migration Order

Next.js's own recommendation, which holds up in practice, is to migrate leaves before roots: start with the pages that have the fewest incoming dependencies (a settings page, a static marketing page) and get more foundational as you gain confidence, saving the routes that share the most infrastructure — such as the ones behind global auth or a layout wrapper every other page reuses — for later. A workable sequence:

  1. Set up app/layout.tsx with the bare minimum: <html>, <body>, and whatever your _document.tsx was doing (fonts, lang attribute).
  2. Migrate leaf pages first — pages with no nested layouts, few dependencies, and low traffic. A /about or /settings page is a good first candidate.
  3. Migrate API routes to Route Handlers as you touch the pages that call them, not all at once — there's no requirement to migrate pages/api/* before pages/*.tsx, or vice versa.
  4. Migrate shared layout structure (nav bars, sidebars) once you understand which pages will share an app/(group)/layout.tsx.
  5. Migrate the global providers from _app.tsx into app/layout.tsx last, once most consuming pages are already Server/Client Components under app/ — this is usually the highest-risk step because it touches every route.
  6. Delete pages/_app.tsx, pages/_document.tsx, and the pages/ directory once nothing remains in it.

Middleware is worth calling out here even though it isn't a step: it needs no migration at all. See the dedicated section below.


getServerSideProps → Server Component Data Fetching

getServerSideProps (GSSP) ran on every request, on the server, and passed its return value to the page as props. In the App Router, a Server Component is the server-side data-fetching layer — there's no separate function, no props handoff, no serialization boundary between "data function" and "component."

Pages Router:

tsx

App Router:

tsx

The mapping:

GSSP contextApp Router equivalent
context.querythe searchParams prop (a Promise in Next.js 15 — must be awaited)
context.params (dynamic routes)the params prop (also a Promise in Next.js 15)
context.req.cookiesawait cookies() from next/headers
context.req.headersawait headers() from next/headers
return { props }just return JSX — no wrapper object
return { notFound: true }call notFound() from next/navigation
return { redirect: { destination } }call redirect() from next/navigation

There is no per-request "props" concept to preserve — the Server Component runs on the server for every request by default (unless the route is statically rendered), so awaiting your data source directly inside the component is the per-request fetch that GSSP used to do.


getStaticProps / getStaticPathsgenerateStaticParams

getStaticProps (GSSP-static) built props at build time; getStaticPaths told Next.js which dynamic paths to prebuild. In the App Router these two responsibilities split differently: generateStaticParams replaces getStaticPaths (it only enumerates the paths), and the page component itself does the fetching that getStaticProps used to do.

Pages Router:

tsx

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.