
App Router vs Pages Router in Next.js: Which One Should You Use in 2026?
Next.js ships with two routers. The Pages Router is the original one: a pages directory where every file is a route, data loaded through getServerSideProps and getStaticProps, and a single _app.tsx wrapping everything. The App Router arrived in Next.js 13, lives in an app directory, and is built on React Server Components, nested layouts, and streaming. Both are still supported in Next.js 16, and both can live in the same project.
That leaves a real question for anyone starting a project or maintaining an older one: which router should you be using in 2026? The short answer is "the App Router for anything new", but the longer answer depends on what you already have, what your team knows, and which features you actually need.
I'll compare the two routers side by side on the things that affect your daily work: how routes are defined, how components render, how data is loaded, how layouts and loading states work, how caching behaves, and how mutations are handled. Then I'll give a concrete decision guide.
The Short Version
If you're starting a new Next.js project today, use the App Router. Every new framework feature since 2023 has landed there first (or only there): Server Components, Server Actions, streaming with Suspense, parallel and intercepting routes, the Metadata API, "use cache", and Cache Components. The official docs default to it, and most of the ecosystem's examples now assume it.
If you have a working Pages Router app, you don't have to rewrite it. The Pages Router is still maintained, still receives fixes, and still works with Next.js 16. You can adopt the App Router incrementally, one route at a time, whenever there's a reason to.
The rest of this post explains why.
How Routes Are Defined
Both routers are file-system based, but they map files to URLs differently.
In the Pages Router, a file is a route:
pages/
├── _app.tsx # wraps every page
├── _document.tsx # custom HTML document
├── index.tsx # /
├── about.tsx # /about
├── blog/
│ ├── index.tsx # /blog
│ └── [slug].tsx # /blog/:slug
└── api/
└── posts.ts # /api/posts
In the App Router, a folder is a route segment, and a special page.tsx file makes it publicly accessible:
app/
├── layout.tsx # root layout (replaces _app and _document)
├── page.tsx # /
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── layout.tsx # shared UI for /blog/*
│ ├── page.tsx # /blog
│ └── [slug]/
│ ├── page.tsx # /blog/:slug
│ └── loading.tsx # loading UI for this segment
└── api/
└── posts/
└── route.ts # /api/posts
The folder-based approach has one practical advantage: you can colocate files. A components/ folder or a utils.ts file inside app/blog/ doesn't become a route because only page.tsx and route.ts are publicly routable. In the Pages Router, every file inside pages/ is treated as a page, so components have to live elsewhere.
The App Router also adds a set of special files per segment: layout.tsx, loading.tsx, error.tsx, not-found.tsx, template.tsx, and default.tsx. Each one handles a specific concern for that part of the URL.
Rendering: Server Components vs Client Components
This is the biggest conceptual difference.
In the Pages Router, every page component is a regular React component. It's rendered to HTML on the server (or at build time) and then hydrated in the browser. All of its code, including any libraries it imports, is shipped to the client.
In the App Router, components are Server Components by default. They run only on the server, can be async, can read from a database directly, and send no JavaScript to the browser for their own logic. You opt into client-side interactivity per component with the "use client" directive.
// app/products/page.tsx
import { AddToCartButton } from "./add-to-cart-button";
import { db } from "@/lib/db";
export default async function ProductsPage() {
const products = await db.product.findMany();
return (
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name}
<AddToCartButton productId={product.id} />
</li>
))}
</ul>
);
}
// app/products/add-to-cart-button.tsx
"use client";
import { useState } from "react";
export function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)} disabled={added}>
{added ? "Added" : `Add ${productId} to cart`}
</button>
);
}
The page queries the database directly and ships no JavaScript for the list itself. Only the button, which needs useState and an onClick handler, becomes part of the client bundle. In the Pages Router, the equivalent page would ship the whole component tree to the browser, and the database query would have to live in getServerSideProps.
For content-heavy pages this can make a noticeable difference in bundle size. The trade-off is a new mental model: you have to think about where the server/client boundary sits and what can be passed across it (serializable props only).
Data Fetching
Pages Router
Data loading happens in page-level functions that run before the component renders:
// pages/dashboard.tsx
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
type Project = { id: string; name: string };
export const getServerSideProps = (async () => {
const res = await fetch("https://api.example.com/projects");
const projects: Project[] = await res.json();
return { props: { projects } };
}) satisfies GetServerSideProps<{ projects: Project[] }>;
export default function Dashboard({
projects,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<ul>
{projects.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
This is explicit and easy to reason about. The downside is that only the page can fetch. If a sidebar component deep in the tree needs data, you either fetch it in the page and prop-drill it down, or fetch it client-side with useEffect, SWR, or TanStack Query.
App Router
Any Server Component can fetch its own data:
// app/dashboard/page.tsx
type Project = { id: string; name: string };
export default async function Dashboard() {
const res = await fetch("https://api.example.com/projects", {
cache: "no-store",
});
const projects: Project[] = await res.json();
return (
<ul>
{projects.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
There's no special function name to remember. The component is async, it awaits its data, and it renders. A sidebar component can do the same thing independently, and identical fetch calls in a single render are deduplicated automatically. For non-fetch data sources, wrap the function in React's cache() to get the same deduplication.
Request data such as cookies and headers come from functions rather than a req object. In Next.js 16 these are async:
// app/settings/page.tsx
import { cookies } from "next/headers";
export default async function SettingsPage() {
const cookieStore = await cookies();
const theme = cookieStore.get("theme")?.value ?? "light";
return <p>Current theme: {theme}</p>;
}
The same goes for params and searchParams, which are now promises you await in pages and layouts.
Layouts
In the Pages Router, _app.tsx wraps every page. For per-section layouts, the community settled on the getLayout pattern: attach a static function to each page and call it from _app. It works, but it's a convention rather than a framework feature, and it doesn't handle nesting well.
In the App Router, layouts are built into the routing model. A layout.tsx in any folder wraps every route below it, and layouts nest automatically:
// app/dashboard/layout.tsx
import Link from "next/link";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="grid grid-cols-[220px_1fr]">
<nav className="flex flex-col gap-2 p-4">
<Link href="/dashboard">Overview</Link>
<Link href="/dashboard/projects">Projects</Link>
<Link href="/dashboard/settings">Settings</Link>
</nav>
<main className="p-6">{children}</main>
</div>
);
}
When you navigate from /dashboard/projects to /dashboard/settings, the layout doesn't re-render. Only the page inside it changes, so any client state in the sidebar (a collapsed section, a search box) is preserved. This "partial rendering" is one of the main reasons navigation feels faster in App Router apps.
Loading and Error States
The Pages Router has no built-in way to show a loading state for a page whose data is still being fetched on the server. During a client-side transition, the old page stays on screen until getServerSideProps resolves. People work around this with router events and a global progress bar.
The App Router has loading.tsx and error.tsx at the segment level:
// app/dashboard/loading.tsx
export default function Loading() {
return <p className="animate-pulse">Loading dashboard...</p>;
}
loading.tsx wraps the page in a React Suspense boundary, so navigation is immediate and the fallback shows while the server streams the real content. You can also place Suspense boundaries around individual components to stream parts of a page independently. error.tsx works the same way for errors: it catches errors in its segment without taking down the whole app.
Caching and Static Generation
The Pages Router has a simple, page-level model:
getStaticPropsrenders at build time.getStaticPropswithrevalidategives you Incremental Static Regeneration.getServerSidePropsrenders on every request.getStaticPathsdecides which dynamic paths to prerender.
The App Router's model is more granular and has changed more over time. In Next.js 16 there are two modes:
- Without Cache Components (the default), routes are prerendered when possible,
fetchis not cached unless you passcache: "force-cache"ornext: { revalidate }, and segment config likeexport const revalidate = 3600controls ISR.generateStaticParamsreplacesgetStaticPaths. - With Cache Components (
cacheComponents: trueinnext.config.ts), everything is dynamic by default and you opt into caching with the"use cache"directive at the function or component level, paired withcacheLifeandcacheTag. This is how Partial Prerendering works in Next.js 16: a static shell is served immediately and dynamic parts stream in.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
The App Router's caching is more powerful, because you can cache a single component instead of a whole page. It's also more to learn. If you've been burned by App Router caching in Next.js 13 or 14, it's worth looking again: the defaults are much less surprising since Next.js 15, and Cache Components makes caching explicit.
Mutations: API Routes vs Server Actions
In the Pages Router, writes go through API routes in pages/api. The client calls them with fetch, and you manually refetch or update client state afterward.
The App Router supports Route Handlers (app/api/.../route.ts) for the same purpose, but it also has Server Actions: async functions marked with "use server" that you can pass directly to a form.
// app/todos/page.tsx
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";
async function addTodo(formData: FormData) {
"use server";
const title = String(formData.get("title") ?? "");
await db.todo.create({ data: { title } });
revalidatePath("/todos");
}
export default async function TodosPage() {
const todos = await db.todo.findMany();
return (
<>
<form action={addTodo}>
<input name="title" required />
<button type="submit">Add</button>
</form>
<ul>
{todos.map((t) => (
<li key={t.id}>{t.title}</li>
))}
</ul>
</>
);
}
The form works before JavaScript loads, the action runs on the server, and revalidatePath refreshes the page data in the same round trip. Treat Server Actions like public endpoints: validate input and check authorization inside every action.
What the Pages Router Still Does Well
The Pages Router isn't obsolete, and there are honest reasons to keep using it:
- Simpler mental model. Everything is a client component, data loads in one place per page, and there's no server/client boundary to think about.
- Built-in i18n routing. The
i18noption innext.config.jshandles locale prefixes and detection for you. The App Router expects you to build this with a[lang]segment andproxy.ts. - Library compatibility. Some older libraries assume they run in the browser and break or need wrappers inside Server Components. In the Pages Router they just work.
- Stability of a large existing codebase. If your app works, is fast enough, and your team is productive, a rewrite has a real cost and often little user-facing benefit.
- React version control. The Pages Router uses the React version in your
package.json, while the App Router uses the React canary channel built into Next.js.
Side-by-Side Comparison
| Concern | Pages Router | App Router |
|---|---|---|
| Route definition | File is a route | Folder is a segment, page.tsx makes it public |
| Default component type | Client (hydrated) | Server Component |
| Data fetching | getServerSideProps, getStaticProps | async components, fetch, any data library |
| Dynamic paths | getStaticPaths | generateStaticParams |
| Shared UI | _app.tsx, getLayout pattern | Nested layout.tsx files |
| Loading UI | Manual (router events) | loading.tsx, Suspense streaming |
| Error UI | _error.tsx, 404.tsx | error.tsx, not-found.tsx per segment |
| Mutations | API routes | Server Actions, Route Handlers |
| Head/SEO | next/head | Metadata API (metadata, generateMetadata) |
| Caching | Page-level SSG/ISR/SSR | Component-level with "use cache" or fetch/segment options |
| Advanced routing | Not available | Parallel routes, intercepting routes, route groups |
| i18n routing | Built-in config | Manual ([lang] segment + proxy) |
A Decision Guide
Starting a new project? Use the App Router. You get the current feature set, the default docs, and the direction the framework is going. Even a simple marketing site or blog benefits from Server Components and the Metadata API, and you won't face a migration later. If you're new to Next.js, start with what Next.js is and setting up TypeScript.
Maintaining a stable Pages Router app? Keep it. Upgrade to the latest Next.js for security and performance fixes, and only move routes to app when you have a concrete reason: a new section of the site, a page that would clearly benefit from streaming, or a feature that only exists in the App Router.
Planning a big new feature in an existing Pages Router app? Build it in app. Both directories can live in the same project. The main catch is that navigating between a Pages route and an App route is a full page load, and next/link doesn't prefetch across routers. Keeping a whole section (say, everything under /dashboard) in one router minimizes that.
Relying heavily on Pages Router i18n or a library that doesn't support Server Components? Check the migration cost carefully first. These are solvable, but they're the most common reasons a migration takes longer than planned.
Running Both Routers Together
Since app and pages can coexist, you don't need a big-bang switch. A few rules apply:
- A URL can only be defined in one router. If both
pages/about.tsxandapp/about/page.tsxexist, the build fails with a conflict. app/layout.tsxdoesn't apply topages/*routes, and_app.tsxdoesn't apply toapp/*routes. Global styles and providers need to be set up in both while you migrate.- Shared components should avoid
next/router(Pages only) andnext/navigation(App only) in the same file.next/compat/routerexists for components used by both.
FAQ
Is the Pages Router deprecated? No. It's still supported and documented in Next.js 16. New features, however, are focused on the App Router.
Is the App Router slower? For most apps it's faster for users, because less JavaScript ships to the browser and layouts don't re-render on navigation. Development builds use Turbopack by default in Next.js 16 for both routers.
Can I use Server Components in the Pages Router? No. Server Components, Server Actions, and streaming with Suspense at the route level are App Router features.
Conclusion
The App Router is the default choice for new Next.js work in 2026. It gives you Server Components, nested layouts, streaming, Server Actions, and a component-level caching model that the Pages Router can't match. The Pages Router is still a reasonable place for an existing app to stay, and it remains simpler to reason about.
If you're on the fence, pick the App Router for new code and move older routes over when there's a reason to touch them. Since the two routers coexist, you can make that decision one route at a time rather than all at once.


