Next.js App Router vs Pages Router
Compare the Next.js App Router and Pages Router: routing, data fetching, layouts, server components, and how to decide for new and existing projects.
What you'll learn
- ✓How routing differs between the two
- ✓Where data fetching lives in each
- ✓Layouts, templates, and nested routes
- ✓Server Components in App Router
- ✓When to migrate vs stay
Prerequisites
- •Comfortable with React
Next.js currently ships two routers in the same framework: the older Pages Router and the newer App Router. Both are supported, both can live in the same project, and the choice between them shapes how you fetch data, share layouts, and structure your code. This post compares the two without taking sides, so you can decide based on your project’s reality.
What and why
The Pages Router (the pages/ directory) is the original Next.js model. Each file is a route. Data is fetched with getServerSideProps, getStaticProps, or getStaticPaths. Everything in pages/ is a client component by React’s old definitions; SSR produces HTML, then the client hydrates.
The App Router (the app/ directory) is a newer model built around React Server Components. Routing is folder-based with conventions: page.tsx is a route, layout.tsx is a nested layout, loading.tsx is a Suspense fallback, error.tsx is an error boundary. Data fetching happens inside async Server Components by await-ing directly.
Both target the same goals: SSR, SSG, ISR, client-side navigation. The App Router pushes more work to the server by default.
Mental model
The Pages Router is “one file, one route, one data function.” The App Router is “a folder tree where each folder can contribute layout, loading, errors, and a page.”
Pages Router:
pages/
index.tsx
blog/
[slug].tsx // dynamic route
index.tsx
api/
hello.ts // API route
App Router:
app/
layout.tsx // root layout (Server Component)
page.tsx // home
blog/
layout.tsx // blog-specific layout
page.tsx // /blog
loading.tsx // Suspense fallback
error.tsx // Error boundary
[slug]/
page.tsx // /blog/:slug
app/api/
hello/route.ts // route handler Hands-on example
Pages Router data fetching:
// pages/blog/[slug].tsx
import { GetServerSideProps } from 'next';
export default function Post({ post }) {
return <article><h1>{post.title}</h1><p>{post.body}</p></article>;
}
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
const res = await fetch(`https://api.example.com/posts/${params!.slug}`);
const post = await res.json();
return { props: { post } };
};
App Router equivalent:
// app/blog/[slug]/page.tsx
export default async function Post({ params }: { params: { slug: string } }) {
const res = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 60 },
});
const post = await res.json();
return <article><h1>{post.title}</h1><p>{post.body}</p></article>;
}
The App Router version is a Server Component. It awaits the fetch directly. No getServerSideProps. The revalidate option puts ISR-like caching control inline with the fetch.
Layouts are persistent across navigation in the App Router:
// app/blog/layout.tsx
export default function BlogLayout({ children }) {
return (
<div>
<aside>Blog sidebar</aside>
<main>{children}</main>
</div>
);
}
The sidebar mounts once and stays mounted as you navigate between /blog/post-a and /blog/post-b. With the Pages Router, layouts traditionally required _app.tsx plus per-page composition.
Server Actions and mutations
In the App Router, you can mutate data from a form by marking a function with "use server":
async function createPost(formData: FormData) {
'use server';
await db.posts.create({ data: { title: formData.get('title') as string } });
revalidatePath('/blog');
}
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}
There is no API route for this case. The Pages Router would require pages/api/posts.ts plus a client fetch.
When to use which
Choose the App Router for new projects that benefit from Server Components: data-heavy dashboards, content sites, marketing pages with personalization. The smaller client bundle and built-in streaming are real wins.
Stick with or migrate gradually to the Pages Router only if you have a large existing codebase, a dependency that does not play well with Server Components (some client-only libraries), or a team unfamiliar with the new model.
You can run both in the same Next.js app. Pages in pages/ take priority for matching URLs.
Common pitfalls
- Marking everything as
"use client"in the App Router. You lose the bundle benefits and end up with a glorified Pages Router. Push the directive as deep as possible. - Trying to use
getServerSidePropsinsideapp/. It does not exist there. Data fetching moves into the component itself. - Forgetting that
paramsandsearchParamsin the App Router can be promises (in recent versions). Await them. - Mixing
next/routerandnext/navigation. The first is for Pages Router, the second for App Router. They are not interchangeable.
Best practices
- Default to Server Components in the App Router and opt into client only where you need state, effects, or browser APIs.
- Use route handlers (
route.ts) instead of API routes in the App Router. - Leverage
loading.tsxanderror.tsxinstead of writing them by hand. They map to Suspense and Error Boundaries automatically. - For migrations, move routes a folder at a time. The two routers coexist gracefully.
FAQ
Is the Pages Router deprecated? Not officially. Next.js continues to support it, but new framework features land in the App Router first.
Can I use Server Components in the Pages Router? No. RSC support is App Router only.
Which is faster? Both can be very fast. The App Router has an edge for data-heavy pages because of smaller client bundles and streaming.
What about Turbopack and the rest? Both routers benefit from Turbopack dev builds. The runtime characteristics depend on the router model, not the bundler.
Related articles
- Next.js Error Boundaries in the Next.js App Router
How error.tsx, global-error.tsx, and not-found.tsx work in the Next.js App Router — when each one fires, how segments isolate failures, and patterns for recovery and observability.
- Next.js Next.js Dynamic Routes and Catch-All Segments Explained
Master dynamic routes, catch-all segments, and optional catch-alls in Next.js. Learn how the file-based router maps URLs to components with concrete examples.
- Next.js Parallel and Intercepting Routes in Next.js
How parallel slots and intercepting routes power dashboards, modals, and tabbed UIs in the Next.js App Router — the file conventions, when to reach for each, and the patterns that hold up in production.
- Next.js Next.js Server Actions Deep Dive
Server Actions explained: what they are under the hood, how to use them for forms and mutations, and the security and UX patterns that matter.