Migrating from the Pages Router to the App Router

Advanced
13 min

Migrating from the Pages Router to the App Router

Many production applications still run on the Pages Router, which remains supported. Moving to the App Router unlocks layouts, Server Components, streaming and Server Actions without a rewrite: both routers coexist in one project, so you migrate a route at a time. After this lesson you will be able to map each Pages Router concept to its App Router equivalent, convert data fetching and navigation code, and use codemods for the mechanical parts.

Concept Map

| Pages Router | App Router | |---|---| | pages/_app.tsx, pages/_document.tsx | app/layout.tsx (root layout) | | pages/404.tsx, pages/_error.tsx | not-found.tsx, error.tsx | | getServerSideProps | async Server Component reading request data | | getStaticProps + revalidate | Cached fetch or "use cache" + revalidate | | getStaticPaths | generateStaticParams | | pages/api/*.ts | app/api/*/route.ts | | next/router (useRouter().query) | next/navigation (useParams, useSearchParams) | | next/head | metadata export or generateMetadata | | middleware.ts | proxy.ts |

Next.js routes each URL to whichever router defines it, so you can move /blog now and /checkout later.

Step 1: Create the Root Layout

_app.tsx and _document.tsx merge into one file that owns <html> and <body>:

tsx
// app/layout.tsx import "./globals.css"; import { Providers } from "./providers"; // "use client" wrapper for Context providers export const metadata = { title: "My App" }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <Providers>{children}</Providers> </body> </html> ); }

Logic around Component and pageProps in _app becomes layout code, and global CSS moves to this import. pages/_app.tsx stays until the last page has moved.

Step 2: Convert Data Fetching

The sample at the top of this lesson shows the most common conversion:

  • getServerSideProps disappears; the page is async and reads cookies(), headers() or searchParams, which makes it dynamic.
  • getStaticProps becomes fetching inside the component. Add cache: "force-cache" or a revalidate window to keep static behaviour, since fetch is uncached by default.
  • getStaticPaths becomes generateStaticParams; fallback: "blocking" maps to the default dynamicParams = true, and fallback: false to dynamicParams = false.

Step 3: Navigation, Metadata and API Routes

tsx
// BEFORE import { useRouter } from "next/router"; const { query, pathname, push } = useRouter(); const slug = query.slug; // AFTER import { useParams, usePathname, useRouter } from "next/navigation"; const { slug } = useParams<{ slug: string }>(); const pathname = usePathname(); const { push } = useRouter();

Replace <Head> with a metadata export or generateMetadata. API routes become Web-standard functions:

typescript
// BEFORE pages/api/hello.ts export default function handler(req, res) { res.status(200).json({ ok: true }); } // AFTER app/api/hello/route.ts import { NextResponse } from "next/server"; export function GET() { return NextResponse.json({ ok: true }); }

Only components using hooks or browser APIs need "use client".

Step 4: Let the Codemods Do the Mechanical Work

bash
npx @next/codemod@latest upgrade latest # upgrade and apply version codemods npx @next/codemod@latest next-async-request-api . # await params, searchParams, cookies() npx @next/codemod@latest middleware-to-proxy . # rename middleware.ts to proxy.ts

Run them on a clean branch and review the diff.

Common mistakes

  • Marking every migrated page "use client" to silence hook errors, forfeiting Server Component benefits.
  • Forgetting that fetch is uncached in the App Router, turning former static pages into dynamic ones.
  • Keeping next/router imports; they throw when used under app/.
Quick Quiz
Question 1 of 3

What replaces `getStaticPaths` in the App Router?

Key Takeaways

  • The Pages and App Routers coexist, so migrate route by route rather than all at once.
  • _app and _document merge into app/layout.tsx; Context providers move to a client wrapper.
  • getServerSideProps becomes an async component; getStaticProps becomes cached fetch; getStaticPaths becomes generateStaticParams.
  • Switch next/router to next/navigation, next/head to metadata, and API routes to route.ts handlers.
  • Use @next/codemod for upgrades, async request APIs and the proxy rename.

Next lesson: Capstone: Building a Full-Stack Task Manager — combine everything from the course into one deployable application.

Migrating from the Pages Router to the App Router - Next.js | CodeYourCraft | CodeYourCraft