Next.js App Router vs Pages Router: Differences and Whether to Migrate
Next.js has two routers: the older pages/ directory and the newer app/ directory built on React Server Components. How routing, data fetching, layouts and API routes differ, which one new projects should use, and a sensible incremental migration path.
Next.js ships two routing systems in the same framework:
- Pages Router — the original, using a
pages/directory. Stable, well understood, and still supported. - App Router — introduced in Next.js 13, using an
app/directory and built on React Server Components. The recommended default for new projects.
They can coexist in one app, which is what makes gradual migration possible.
Side by side
Pages Router (pages/) |
App Router (app/) |
|
|---|---|---|
| Route file | pages/blog/[slug].tsx |
app/blog/[slug]/page.tsx |
| Default component type | Client (hydrated) | Server Component |
| Data fetching | getServerSideProps, getStaticProps |
async components, fetch, direct DB calls |
| Layouts | _app.tsx + manual wrappers |
Nested layout.tsx per folder |
| Loading / error UI | Manual | loading.tsx, error.tsx, not-found.tsx |
| Mutations | API routes + client fetch | Server Actions (plus route handlers) |
| API endpoints | pages/api/*.ts |
app/api/*/route.ts (route handlers) |
| Metadata / SEO | next/head |
metadata export / generateMetadata |
| Streaming | Limited | Built in with Suspense |
| Caching model | Simpler | More layers to understand |
Routing
Pages Router: the file is the route.
pages/index.tsx → /
pages/blog/[slug].tsx → /blog/:slug
pages/api/users.ts → /api/users
App Router: folders are routes; special files inside define what renders.
app/page.tsx → /
app/blog/[slug]/page.tsx → /blog/:slug
app/blog/layout.tsx → wraps every /blog page
app/blog/[slug]/loading.tsx → shown while the page loads
app/api/users/route.ts → /api/users
Folders in (parentheses) group routes without affecting the URL, and files like components.tsx placed in a route folder aren't routes unless named page or route.
Data fetching
Pages Router:
export async function getServerSideProps({ params }) {
const post = await db.post.find(params.slug)
return { props: { post } }
}
export default function Post({ post }) { … }
App Router:
export default async function Post({ params }) {
const { slug } = await params
const post = await db.post.find(slug)
return <article>…</article>
}
The App Router version is shorter, and the data fetching sits next to where it's used. Each component can fetch what it needs.
Why the App Router is the default now
- Less JavaScript to the browser, since most components stay on the server.
- Nested layouts that persist between navigations (no re-rendering the sidebar).
- Streaming — show parts of the page as their data arrives.
- Server Actions for forms without hand-written API routes.
- New Next.js features land here first.
Why some stay on Pages
- It works. A stable Pages app has no urgent reason to move.
- Mental model. Server vs client components, caching and revalidation have a learning curve, and mistakes cause confusing bugs.
- Library compatibility. Some older libraries assume everything is client-side and need
"use client"wrappers.
Choosing for a new project
Use the App Router. It's what the docs, examples and AI tools default to now. If an AI tool generates getServerSideProps in a new App Router project, it's mixing patterns — tell it which router you're using in your CLAUDE.md.
Migrating incrementally
You don't need a big-bang rewrite. Both directories work together:
- Upgrade Next.js on the Pages Router first and fix anything that breaks.
- Create
app/layout.tsx(the root layout replacing_appand_documentfor app routes). - Move one route at a time, starting with simple, mostly static pages. A route must exist in only one directory.
- Convert
getServerSideProps/getStaticPropsintoasyncServer Components; move interactive parts into small"use client"components. - Replace
next/headwith themetadataexport. - Move API routes to route handlers when convenient —
pages/apikeeps working meanwhile. - Test each moved route (navigation between old and new routes does a full page load until both sides are on the same router).
Coding agents are good at this kind of mechanical, route-by-route migration — especially with a test suite and a running app to check against. (Using Claude Code on a large codebase)
Self-hosting either
Both routers work with next build && next start on any Node server. (Self-hosting Next.js)
The summary
- Pages Router:
pages/,getServerSideProps, client components by default. - App Router:
app/, Server Components, nested layouts, Server Actions, streaming. - New projects → App Router. Stable Pages apps → migrate when there's a reason.
- Migrate route by route; both routers can run side by side.
EasySpawn runs Next.js on a persistent server with Postgres alongside, so Claude Code can migrate routes, run the app and check each page as it goes. See how it works or join the waitlist.
Related: React Server Components Explained · What Is Next.js? · Astro vs Next.js · Self-Hosting Next.js Without Vercel
Keep reading
TanStack Query vs useEffect for Data Fetching in React
Fetching in useEffect looks simple until you need loading states, errors, caching, race conditions, refetching and mutations. What TanStack Query handles for you, side-by-side code, mutations with invalidation, and when server components or plain useEffect are still the right choice.
React State Management: useState vs Context vs Zustand vs Redux
Most React apps need less state management than they think. Sort your state into server, URL, form, local and global, then pick the lightest tool for each: useState, the URL, React Context, Zustand, Redux Toolkit or Jotai. Trade-offs, examples and common mistakes.