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.
| 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.
_app.tsx and _document.tsx merge into one file that owns <html> and <body>:
// 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.
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.// 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:
// 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".
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.tsRun them on a clean branch and review the diff.
"use client" to silence hook errors, forfeiting Server Component benefits.fetch is uncached in the App Router, turning former static pages into dynamic ones.next/router imports; they throw when used under app/.What replaces `getStaticPaths` in the App Router?
_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.next/router to next/navigation, next/head to metadata, and API routes to route.ts handlers.@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.