Confession: I started a new client project two weeks ago and reached for pages/ again. App Router is the default, the docs lead with it, every conference talk is about Server Components, and I still picked the older one. Felt mildly heretical typing npx create-next-app --no-app.
I’m not anti App Router. I shipped a big RSC migration last year and wrote about what stuck. But the more I use both in production, the more I think the “App Router or you’re behind” framing is doing teams a disservice. Pages Router is not legacy. It’s a different shape, and there are real projects where it’s the better shape.
Short version for the impatient: if your app is mostly client-rendered, mostly form-heavy, has an existing data layer (tRPC, Apollo, React Query), or your team is hiring junior React devs in the next quarter, Pages Router is still a reasonable default in 2026. If you want streaming, partial pre-rendering, or want server-first data fetching to be the path of least resistance, App Router earns its keep.
Here’s what I keep coming back to.
The mental model is smaller, and that matters
Pages Router has roughly four concepts to learn: a file is a route, you export getStaticProps or getServerSideProps if you need data, you export getStaticPaths for dynamic builds, and _app.tsx wraps everything. That’s it. The rest is React you already know.
App Router adds Server Components, Client Components, the "use client" boundary, server actions, route groups, parallel routes, intercepting routes, three layers of caching, and the loading.tsx / error.tsx / not-found.tsx file conventions. The Next.js docs page on caching alone is longer than the entire Pages Router data fetching guide. I’ve been writing React since 2017 and I still occasionally forget whether revalidatePath flushes the Data Cache or the Full Route Cache (it’s both, by the way, but only if there’s actually something cached).
That cognitive load is not free. On a small team where one person owns the frontend, it’s manageable. On a team where you’re onboarding two new hires a quarter, every hour spent explaining the difference between “client component that imports a server component” and “server component that renders a client component” is an hour not spent shipping.
A client I worked with this year had three React devs, all good. They moved a real app to App Router and shipped three subtle hydration bugs in a month because the rules around where you can call which hook are genuinely confusing. Two of those bugs were “this works locally and breaks in production with no error in the console” cases. I’m not making this up to be dramatic; the App Router error surface is just larger.
Pages Router plays nicely with existing data layers
Most production React apps I see still have tRPC, React Query, Apollo, or SWR doing the heavy lifting for data. App Router wants you to do data fetching in Server Components and pass plain props down. That’s a lovely model when you’re starting fresh. It’s a slog when you have a year of useQuery calls that already work.
Here’s the kind of thing I mean. In Pages Router, this is how I’d ship a dashboard page that needs auth and live data:
// pages/dashboard.tsx
import { useQuery } from "@tanstack/react-query";
import { withAuth } from "@/lib/auth";
function Dashboard() {
const { data } = useQuery({ queryKey: ["metrics"], queryFn: fetchMetrics });
return <Charts metrics={data} />;
}
export default withAuth(Dashboard);
That’s it. The withAuth HOC wraps the page. React Query handles the request, caching, and refetching. If I add another tab, I just add another useQuery call.
The App Router equivalent involves picking where the auth check happens (middleware? layout? server component?), deciding whether fetchMetrics belongs in a server component or stays as a client mutation, and then thinking carefully about whether the page should be dynamic or use PPR. Here’s a rough sketch of what I’d actually write:
// app/dashboard/page.tsx
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { ChartsClient } from "./charts.client";
export default async function Page() {
const session = await auth();
if (!session) redirect("/login");
const metrics = await fetchMetrics(session.userId);
return <ChartsClient initialMetrics={metrics} />;
}
Then charts.client.tsx re-hydrates React Query with initialMetrics so the client can refetch on the same key. It works fine, but it’s two files where it used to be one, and you have to know the dance to make React Query and Server Components co-exist. The TanStack Query docs on hydration cover this honestly, including a section that boils down to “this is a bit of a workaround”.
Static site generation in Pages Router is still the cleanest API
If you’re building a marketing site or a docs site, the getStaticProps / getStaticPaths pair is hard to beat. The contract is dead simple. You return data, Next generates HTML at build time, the result is a CDN-cacheable file.
App Router’s equivalent is to fetch in a Server Component, let the framework infer it’s static, and trust the caching headers. That works most of the time. It also fails in interesting ways when you accidentally pass a headers() call somewhere deep in the tree and your “static” page silently becomes dynamic. I’ve watched Vercel deployment logs flip a page from static to dynamic between commits because someone added a third-party analytics import that used headers() internally.
Pages Router doesn’t have that failure mode. If you didn’t export getServerSideProps, the page is static. You can read it in two seconds. I cover this kind of “load-bearing simplicity” thinking in my work on small developer tools, where the cheap, predictable option usually wins over the powerful, opaque one.
When App Router is the right call
I’m not arguing Pages Router for everything. App Router earns its keep when:
- You want streaming as a first-class feature.
loading.tsxand Suspense boundaries are genuinely nice and a real pain to retrofit into Pages Router. - You want partial prerendering. PPR is App Router only, and for content-heavy pages with a small interactive island, it’s the right model. I wrote about eight months of shipping React Server Components and PPR is the part I’d miss most if I went back.
- Your data layer is already server-first. If you’re calling Postgres directly from API routes anyway, moving those calls into Server Components is a small refactor and a real win.
- You want server actions instead of REST endpoints for forms. They feel weird at first and then you stop wanting to write API routes for every mutation.
The unlock is real. I just don’t think it’s universal.
What I actually do in practice
For new projects, I run a quick checklist before I pick:
- Will this be primarily marketing or content? App Router with PPR.
- Is it a SaaS dashboard with an existing tRPC or React Query setup? Pages Router. Don’t fight your data layer.
- Mostly forms with server-side validation? App Router for server actions if you’re starting fresh. Pages Router with API routes if the rest of the codebase already does that.
- Is the team familiar with RSC, or is everyone new to it? If new, Pages Router until at least one person on the team has read the App Router caching docs twice.
I’ve shipped two production apps this year on Pages Router with Next 15, and the upgrade from Next 14 was a package.json bump and three minor type changes. The Pages Router is not going anywhere. The Next team has been clear that it’s a long-term supported API, not a deprecation path.
One concrete thing to try this week
If you’re on App Router and find yourself fighting the framework more than shipping, try this: spin up a fresh Pages Router project, port one of your pages over, and ship it as a sibling app behind a subdomain. Give yourself a week. See if you miss anything.
Half the time, you’ll come back to App Router with a list of three things you actually wanted (probably streaming, layouts, and server actions) and you can use them deliberately. The other half, you’ll keep the Pages Router app and feel sane again. Both outcomes are useful. Picking the framework that fits your team is more important than picking the one on the conference slides.