Web / Next.js
Next.js App Router vs Pages Router: Which Should You Use in 2026?
The App Router should be your default for every new Next.js project in 2026: it is the only router where layouts nest, loading and error states are file-level primitives, streaming works out of the box, and React Server Components eliminate client-side JavaScript by default. The Pages Router remains fully supported and is still the right choice for existing applications that are stable, feature-complete, and not blocked on streaming or server components.
A Short History of the Two Routers
The Pages Router is the original Next.js routing system. It shipped with the framework in 2016 and powered every version through Next.js 12: routes map to files in a pages/ directory, data enters a page through getServerSideProps, getStaticProps, or getInitialProps, and everything rendered to HTML on the server is hydrated as a React client component. That mental model — one component tree, rendered twice — is simple, and a decade of tutorials, libraries, and production apps are built on it.
The App Router became stable in Next.js 13.4 in May 2023 and was hardened through the Next.js 14 and 15 releases: routes live in an app/ directory, pages are React Server Components by default, and data fetching is an ordinary async function call rather than a special export. By 2026 the App Router is what the Next.js team ships features for first: the caching model, Server Actions, Partial Prerendering, and typed routes all assume app/.
Both routers ship in the same framework today. You can even run them side by side in one project during a migration. The question is no longer "is it stable?" — it is "which constraints does my project actually have?"
Data Fetching
This is the biggest conceptual shift. In the Pages Router, data fetching is a framework-level convention: special exported functions run before the component, and their return value is passed in as props. In the App Router, data fetching is just JavaScript — an async Server Component can await a fetch, a database query, or an ORM call directly, anywhere in the tree.
Two practical consequences follow. First, you can colocate data requirements: a sidebar component fetches its own data instead of hoisting everything to the page. Second, the fetch API inside app/ is memoized and cache-aware, with next: { revalidate: 60 } controlling staleness per request.
// pages/products/[slug].tsx — Pages Router
export async function getServerSideProps(context) {
const { slug } = context.params;
const product = await db.products.findBySlug(slug);
return { props: { product } };
}
export default function ProductPage({ product }) {
const [reviews, setReviews] = useState([]);
useEffect(() => {
fetch(`/api/reviews/${product.id}`).then((r) => r.json()).then(setReviews);
}, [product.id]);
return <ProductDetail product={product} reviews={reviews} />;
}The same feature in the App Router drops the special export and the useEffect fetch entirely, because the child component that needs reviews can fetch them on the server:
// app/products/[slug]/page.tsx — App Router
export default async function ProductPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const product = await db.products.findBySlug(slug);
return (
<>
<ProductDetail product={product} />
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={product.id} />
</Suspense>
</>
);
}Verdict: the App Router wins on flexibility and colocated fetching; the Pages Router wins on predictability, because getServerSideProps always runs on the server before render and never accidentally ships a secret to the client. In the App Router you must be deliberate about which components are client components, since a "use client" boundary turns everything below it back into the Pages Router mental model.
Layouts and Nesting
The Pages Router has no first-class layout primitive. The standard workaround is a Higher-Order Component — wrap every page in withLayout(Page) — or accept a remount of the whole shell on navigation, which resets scroll position and re-fetches whatever the shell needed. _app.tsx gives you one global layout and nothing in between.
The App Router treats a layout as a file. A layout.tsx wraps every route beneath it, persists across client-side navigation, and nests to arbitrary depth. Route groups in parentheses let you give different sections of the site different shells without affecting the URL.
// app/(marketing)/layout.tsx — shared layout for a route group
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="marketing-shell">
<SiteHeader />
<main>{children}</main>
<SiteFooter />
</div>
);
}
// app/(marketing)/pricing/page.tsx renders inside that layout
// app/(app)/dashboard/page.tsx does not — different group, different shellBecause layouts do not remount on navigation, state inside a layout survives route changes — a persistent audio player, an open sidebar, a draft editor. That single property removes most of the state-restoration code teams end up writing in the Pages Router.
Loading and Error States
In the Pages Router, loading and error UX is hand-rolled: a spinner in useEffect, a getServerSideProps try/catch that redirects, an Error Boundary component you wire up manually. Nothing about the file system expresses "this route is loading" or "this route failed."
The App Router makes both file-level conventions: loading.tsx renders instantly while the segment's data resolves, and error.tsx is a React Error Boundary scoped to that segment — it can be as deep as you like, so a failing widget does not take down the page. A global not-found.tsx handles 404s and global-error.tsx catches render failures at the root. Because these files wrap segments rather than whole pages, error recovery preserves layout and navigation state.
Streaming and Suspense
Streaming is where the two routers diverge most sharply. The Pages Router sends one HTML document after the slowest getServerSideProps promise settles — there is no way to flush shell HTML early and fill in slow regions afterward, short of hand-building endpoints outside Next.js.
The App Router streams by default. React renders on the server, the shell is flushed as soon as it is ready, and anything wrapped in <Suspense> streams in when its data arrives. The loading.tsx file is syntactic sugar for a Suspense boundary around the whole segment. Combined with await inside the component, this gives you granular, per-component skeletons instead of a full-page spinner.
For Largest Contentful Paint, the difference is measurable: a hero rendered from cached data ships in the first chunk while a slow recommendations call streams in behind it. On the Pages Router, that same slow call delays the entire response. Partial Prerendering extends this further, shipping a static shell immediately and streaming the dynamic holes — but it is an App Router feature only.
Colocation
The Pages Router forces a strict separation: components that run on the server (data functions) live at the top of the file, components that run on the browser live anywhere, and anything imported by a page ends up in the client bundle unless you carefully code-split it. A page directory becomes a flat list of routes with components exiled to components/ two folders away.
The App Router colocates. A route folder can contain the page, its layout, its loading and error files, unit-test files, and helper modules that never reach the browser — because Server Components have no client runtime, anything imported beneath a server boundary is excluded from the bundle automatically. Colocation also scales horizontally: feature folders own their routes, tests, and data access together.
API Routes vs Route Handlers
Pages Router API routes live in pages/api/ and export one function per HTTP verb — GET, POST — receiving a Next.js-specific req/ res pair. Route Handlers in app/api/ use the Web-standard Request and Response objects, can export dynamic segment config, and run on Node.js or the Edge runtime without changing your handler signature.
// app/api/reviews/route.ts — Route Handler
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const productId = searchParams.get("productId");
const reviews = await db.reviews.findMany({ productId });
return NextResponse.json(reviews);
}
export async function POST(request: Request) {
const body = await request.json();
const created = await db.reviews.create(body);
return NextResponse.json(created, { status: 201 });
}The practical advantage: handlers written against Web standards are portable. Code that reads request.json() and returns NextResponse.json() can move to a worker runtime, a different framework, or a test harness with minimal changes. Route Handlers also support streaming responses and Webhooks in a file that coexists with the pages that consume them.
TypeScript and Component Conventions
Both routers support TypeScript, but the App Router bakes stricter conventions into the framework. Page, layout, and route params are typed through generated .d.ts files, so renaming a dynamic segment changes your types and surfaces every broken call site at compile time. With typed routes enabled, internal <Link href="/products/[slug]"> calls become checked as well.
Component conventions differ too. The App Router makes "use client" an explicit opt-in boundary rather than an implicit default, and hooks like useRouter, usePathname, and useSearchParams moved from next/router to next/navigation. Server-only libraries — database clients, SDKs with secrets, heavy parsers — become safe to import directly into a component, guarded by import "server-only" when you want a hard build error if they leak clientward.
Migration Cost
There is no automated one-shot migrator that converts an arbitrary Pages Router app into an App Router app, because the change is architectural rather than syntactic. The realistic path is incremental: keep pages/ and add app/ — Next.js resolves app/ first for any route that exists in both, so you can move one route at a time and keep the rest working.
Typical migration work, in rough order of effort:
- Create a root layout — move the contents of _app.tsx and _document.tsx into
app/layout.tsx, including fonts, metadata, and the html/body tags. - Split data functions into fetches — rewrite
getStaticPropsasfetchwithrevalidateandgetServerSidePropsas dynamicfetchor a database call. - Mark client boundaries — add
"use client"to components using hooks or browser APIs, then try removing it from each parent. - Move API routes —
pages/api/*can stay put indefinitely while new handlers land inapp/api/*. - Replace head management —
next/headandgetServerSidePropsmetadata become the exportedmetadataobject orgenerateMetadata.
For a marketing site of 20 pages this is a day of work. For a large application with hundreds of routes, deep third-party integrations, and Pages-Router-only libraries, it is a multi-quarter project — which is exactly why the incremental coexistence model exists.
Ecosystem and Library Maturity
In 2026 the library ecosystem has largely converged. The major state, form, table, auth, and data-fetching libraries ship App Router guides, and most component libraries work unchanged as long as you add a client boundary. Server Actions and "use server" have no Pages Router equivalent at all.
Gaps that still matter are narrower than they were: libraries that assume browser globals at module scope, packages that were never SSR-safe, and analytics or feature-flag SDKs whose documented patterns still center on _app.tsx. If your stack leans on a specific unmaintained package, check its App Router support before committing to a rewrite — a library pinned to the Pages Router is a legitimate reason to delay.
When the Pages Router Is Still the Right Call
The Pages Router is not deprecated, and choosing it is not a mistake. Three situations genuinely favor it:
- Stable legacy applications — a mature product with no streaming requirement, a green performance budget, and a backlog of feature work far more valuable than a router rewrite. Next.js continues to ship security and bug fixes to the Pages Router.
- Simpler mental model — one component tree, props in from the top, everything client-side. For teams new to React, or for prototypes where server/client boundaries add cognitive overhead with no payoff, the Pages Router is easier to reason about.
- Pages-Router-only dependencies — middleware patterns, custom servers, or libraries that have never been updated for
app/. Migration cost here is real and should be weighed against the benefit, not assumed away.
Decision Table
Run your project through this table before deciding. The "App Router" column is the default for a reason, but any single row can flip the answer.
| Situation | Recommended | Why |
|---|---|---|
| New project, any size | App Router | All new features ship here first |
| Content site, docs, blog | App Router | Static + ISR + streaming shells |
| Dashboards with slow data panels | App Router | Per-section Suspense streaming |
| Existing Pages app, stable | Stay on Pages Router | Rewrite ROI is usually negative |
| Pages-only library in critical path | Pages Router, revisit later | Migrate when the library catches up |
| Team new to React | Either; Pages if simplicity wins | Fewer boundaries to explain |
| Needs Server Actions or PPR | App Router only | No Pages Router equivalent |
| Partial migration underway | Run both, move route by route | app/ wins conflicts on shared paths |
Verdict for New Projects
Use the App Router for new projects. File-level layouts, loading and error states, streaming, colocated data fetching, Server Components, Server Actions, Route Handlers, and Partial Prerendering are not incremental conveniences — together they change the performance profile and the code structure of an application. Starting a 2026 project on the Pages Router means opting out of the entire direction of the framework on day one.
The corollary is equally important: do not migrate a healthy Pages Router application just because the App Router exists. Rewrite when you have a concrete reason — streaming, a bundle size problem, a redesign that touches every route anyway — and move incrementally, route by route, with both routers running side by side until the last pages/ file is gone.
Conclusion
The App Router vs Pages Router debate is mostly settled for new work: the App Router is the future of Next.js, it is stable, and it is where the framework's most valuable features land. The Pages Router remains a supported, production-grade option for applications that already work and do not need what the App Router uniquely provides.
The wrong answer is neither router — it is spending two quarters migrating a profitable legacy app for architectural purity, or starting a greenfield project on a routing model the framework no longer optimizes for. Pick deliberately, migrate incrementally, and let the actual requirements of your application decide.
Key Takeaways
- The App Router is the default for new Next.js projects in 2026; the Pages Router remains supported but receives features second.
- App Router advantages: nested file layouts, file-level loading/error states, streaming via Suspense, colocated data fetching, and zero client JS by default.
- Pages Router advantages: a simpler single-tree mental model, no client/server boundary to manage, and a decade of battle-tested libraries and tutorials.
- Migration is incremental, not a rewrite: add an app/ directory alongside pages/ and move route by route — app/ resolves first on conflicts.
- Migrate only with a concrete trigger (streaming, bundle size, a full redesign); a stable Pages Router app with good metrics does not need to move.
FAQ
Should I use App Router or Pages Router in 2026?
Use the App Router for all new Next.js projects in 2026. It is stable, it is where new features land first, and it gives you streaming, nested layouts, loading/error files, and Server Components. Keep an existing Pages Router app on Pages Router unless you have a specific reason to migrate, such as needing streaming or cutting client-side JavaScript.
Is the Pages Router deprecated?
No. The Pages Router ships in every current Next.js release and continues to receive fixes and security patches. It is not deprecated, but the App Router is the priority for new features such as Partial Prerendering, Server Actions, and typed routes. Running both routers in one project during a migration is officially supported.
Can I migrate from Pages Router to App Router incrementally?
Yes. Add an app/ directory next to your existing pages/ directory, create a root layout, and move one route at a time. When a route exists in both directories, the App Router version takes precedence, so the rest of your site keeps working. API routes in pages/api/ can remain in place while you add new route handlers under app/api/.
Does the App Router send less JavaScript to the browser?
Usually yes. Components in the App Router are Server Components by default and ship no JavaScript at all; only files marked with "use client" — plus their imports — are bundled for the browser. The Pages Router renders and hydrates every page component, so equivalent apps typically ship meaningfully less client JavaScript on the App Router.
Do API routes work the same in both routers?
Functionally yes, syntactically no. Pages Router API routes live in pages/api/ and export Next.js-specific GET/POST functions over a req/res pair. App Router Route Handlers live in app/api/, export the same named functions, but use Web-standard Request and Response objects, which makes them portable across runtimes and easier to test outside Next.js.
References
- Next.js. "App Router Introduction." Next.js Docs. nextjs.org/docs/app
- Next.js. "Pages Router." Next.js Docs. nextjs.org/docs/pages
- Next.js. "Migrating to App Router." Next.js Docs. nextjs.org/docs/app/building-your-application/upgrading/app-router-migration
- React. "Server Components." react.dev. react.dev/reference/rsc/server-components
- React. "Suspense." react.dev. react.dev/reference/react/Suspense
- Next.js. "Routing: Route Handlers." Next.js Docs. nextjs.org/docs/app/building-your-application/routing/route-handlers