Next.js75 min total · 12 parts
Next.js Fundamentals: The App Router, Server Components, and the Caching Rules That Just Flipped
Part 3 of 12 · ~3 min
App Router vs Pages Router
Search "Next.js routing" and you'll land on documentation, and older job postings, describing two entirely separate systems: the App Router (the app/ folder, the current default, and what everything below is built on) and the Pages Router (the pages/ folder, which is how essentially every Next.js app was built before the App Router existed, and plenty still are).
The App Router isn't change for its own sake. The Pages Router ran into a structural wall it genuinely couldn't get past on its own. A page there is fundamentally one component, rendered once, shipping its JavaScript to the browser regardless — there was no way to say "this branch of the tree should never touch the browser at all." Fetching lived in one function attached to the whole page (getServerSideProps, getStaticProps), so a component three levels deep that needed data of its own couldn't just go get it — every request funneled through that single top-level function first, no matter how deep the actual need for the data sat. And a page was slow as one unit: the browser saw nothing until every piece of data that function asked for had come back, fast piece or slow.
Three capabilities the Pages Router had no way to offer are what the App Router is actually built around:
- React Server Components, which can run entirely on the server and reach for their own data wherever they sit in the tree, instead of every request having to pass through one function at the top.
- Layouts that nest, so shared UI wraps a subtree of routes and stays put across navigation inside it, rather than being rebuilt from scratch on every page.
- Streaming, so a response can send its ready parts immediately and fill in the slow parts afterward, instead of one slow data source holding the whole page hostage.
None of that retires the Pages Router. You'll keep meeting it, and there are honest reasons to reach for it on purpose:
- A large app that's already built on it. Rewriting a working app's entire routing and data layer is a genuinely risky project. Plenty of teams never fully migrate, and plenty of profitable apps will keep running the Pages Router for years.
- A flatter model for an app that doesn't need Server Components. Getting a plain object of props back from one function is just an easier thing to hold in your head than sorting out, component by component, which half of the tree is even allowed to touch a browser API. A small app that isn't juggling much data has little to gain from the extra machinery, and picking the simpler router there is a sound call, not a corner cut.
| Pages Router | App Router | |
|---|---|---|
| Where it lives | pages/ | app/ |
| Getting data in | One exported function per page — getServerSideProps, getStaticProps | Any Server Component can await fetch(...) (or any async call) on its own |
| Server-only code as a concept | Doesn't really exist | The default — every Server Component is server-only |
| Shared layout | Hand-built, usually a custom _app.js every page passes through | layout.tsx files scoped to a folder, nesting naturally |
| Sending partial responses early | No | Yes, through Suspense |
| Where you'll run into it | Legacy codebases, older tutorials, take-home interview repos | New projects — OpenRoles, and everything below |