App Router vs Pages Router
The Architectural Shift That Split Next.js in Two
LinkedIn Hook
Next.js has TWO routing systems — and using the wrong one in an interview can make you look 2 years behind.
The Pages Router (
pages/) shipped in 2016. The App Router (app/) shipped in 2023. They look similar on the surface, but under the hood they are fundamentally different architectures.Here's the kicker: most production apps still run Pages Router, but interviewers want you to know the App Router — because it's the future.
In Lesson 1.2, you'll learn exactly how both routers work, what changed and why, a side-by-side comparison table you can recite in interviews, and the migration traps that catch even senior developers.
Read the full lesson -> [link]
#NextJS #AppRouter #PagesRouter #ReactServerComponents #WebDevelopment #FrontendInterview #InterviewPrep
What You'll Learn
- How file-based routing works in both
pages/andapp/— with side-by-side code - The real reason App Router exists (React Server Components, not just folder reorganization)
- A comparison table covering layouts, data fetching, rendering, and more
- Migration considerations and which router interviewers expect you to know
The Restaurant Analogy — Two Kitchens, One Restaurant
Imagine a restaurant that has been serving food for years with a kitchen in the back. The waiters (client) take orders, walk to the kitchen (server), grab the prepared dish, and bring it to the table. This is the Pages Router — it works, it's reliable, and millions of meals have been served this way.
Now the owner decides to renovate. They build a second kitchen right next to the dining area — a pass-through kitchen where chefs can hand dishes directly to diners without a waiter carrying them across the room. Some dishes still come from the back kitchen (client components), but most are now prepared and served right at the pass-through window (server components). This is the App Router.
The restaurant didn't tear down the old kitchen. Both kitchens can run at the same time. But new menu items are designed for the pass-through kitchen, and the owner recommends all new recipes use it.
That's Next.js today: two routing systems coexisting, but the App Router is the recommended path forward.
A Brief History — Why Two Routers Exist
Understanding the timeline helps you explain this confidently in interviews.
2016 — Pages Router ships with Next.js 1.0. Every file in pages/ becomes a route. pages/about.js maps to /about. Simple, elegant, revolutionary at the time.
2016-2022 — Pages Router evolves. getStaticProps, getServerSideProps, and getStaticPaths are added. API routes arrive in pages/api/. The router handles SSR, SSG, and ISR. It becomes the de facto standard for React SSR.
2022 — React Server Components (RSC) are announced. React itself introduces a new paradigm: components that run ONLY on the server and send zero JavaScript to the client. The Pages Router architecture cannot support this — it was designed around the assumption that every component eventually runs on the client.
2023 — Next.js 13.4 makes App Router stable. A completely new routing system built from the ground up to support RSC, nested layouts, streaming, and the new React architecture.
The key insight for interviews: The App Router isn't just a folder rename. It's a fundamentally different architecture designed around React Server Components. The Pages Router sends JavaScript for every component. The App Router sends JavaScript only for components that need interactivity.
File-Based Routing — Side by Side
Both routers use file-based routing, but the conventions are completely different.
Pages Router — The file IS the route
pages/
index.js -> /
about.js -> /about
blog/
index.js -> /blog
[slug].js -> /blog/my-post
api/
users.js -> /api/users
In the Pages Router, the file name directly maps to the URL. The default export of each file is the page component:
// pages/about.js — Pages Router
import { useState } from "react";
export default function AboutPage() {
const [count, setCount] = useState(0);
return (
<div>
<h1>About Us</h1>
<p>Clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
// Every component is a client component by default
// useState, useEffect, event handlers — all work everywhere
// This entire component's JS is shipped to the browser
App Router — The folder IS the route
app/
page.js -> /
layout.js -> Root layout (wraps everything)
about/
page.js -> /about
blog/
page.js -> /blog
[slug]/
page.js -> /blog/my-post
api/
users/
route.js -> /api/users
In the App Router, folders define routes, and special files inside those folders define behavior. The page.js file is what makes a route publicly accessible:
// app/about/page.js — App Router
// This is a Server Component by default — no "use client" needed
export default function AboutPage() {
// NO useState here — this runs on the server only
// NO useEffect — there is no browser lifecycle
// NO event handlers — there is no client-side JS for this component
return (
<div>
<h1>About Us</h1>
<p>This HTML is rendered on the server and sent as static HTML</p>
<p>Zero JavaScript shipped to the browser for this component</p>
</div>
);
}
If you need interactivity, you explicitly opt in:
// app/about/counter.js — Client Component in App Router
"use client"; // This directive makes it a client component
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
// app/about/page.js — Composing server and client components
import Counter from "./counter"; // Client component imported into server component
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>This paragraph is server-rendered — zero JS</p>
<Counter /> {/* Only THIS part ships JavaScript to the browser */}
</div>
);
}
Special Files — The App Router's Secret Weapon
The Pages Router has one convention: the default export is the page. The App Router introduces special files that control different aspects of a route segment:
| File | Purpose | Pages Router Equivalent |
|---|---|---|
page.js | The UI for a route (makes it publicly accessible) | The file itself (about.js) |
layout.js | Shared UI that wraps child routes (persists across navigation) | _app.js (but only one level) |
loading.js | Loading UI shown while the page loads (Suspense boundary) | Manual useState + spinner |
error.js | Error UI shown when something crashes (Error Boundary) | Manual ErrorBoundary component |
not-found.js | 404 UI for this route segment | pages/404.js (global only) |
template.js | Like layout but re-mounts on every navigation | No equivalent |
route.js | API endpoint (replaces pages/api) | pages/api/endpoint.js |
Layouts — The Biggest Architectural Win
The Pages Router has one global layout via _app.js. If you need different layouts for different sections (marketing site vs dashboard), you have to hack around it:
// pages/_app.js — Pages Router: ONE global layout
export default function App({ Component, pageProps }) {
return (
<GlobalLayout>
<Component {...pageProps} />
</GlobalLayout>
);
}
// Want a different layout for /dashboard?
// You have to use per-page layouts — a workaround pattern:
DashboardPage.getLayout = function getLayout(page) {
return <DashboardLayout>{page}</DashboardLayout>;
};
The App Router supports nested layouts natively. Each folder can have its own layout.js, and they nest automatically:
// app/layout.js — Root layout (required, wraps entire app)
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
// app/dashboard/layout.js — Dashboard layout (wraps all /dashboard/* routes)
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
);
}
// Visiting /dashboard/settings renders:
// RootLayout > DashboardLayout > SettingsPage
// The Sidebar NEVER re-renders when navigating between dashboard pages
Data Fetching — The Paradigm Shift
This is where the two routers differ most dramatically, and it is the most common interview question.
Pages Router — Special exported functions
// pages/blog/[slug].js — Pages Router data fetching
// Runs at BUILD time — SSG
export async function getStaticProps({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`);
const data = await post.json();
return {
props: { post: data }, // Passed to the component as props
revalidate: 60, // ISR: regenerate every 60 seconds
};
}
// Tells Next.js which dynamic paths to pre-render
export async function getStaticPaths() {
const posts = await fetch("https://api.example.com/posts");
const data = await posts.json();
return {
paths: data.map((post) => ({ params: { slug: post.slug } })),
fallback: "blocking",
};
}
// Runs at REQUEST time — SSR
export async function getServerSideProps({ req, res, params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`);
const data = await post.json();
return {
props: { post: data },
};
}
// The component receives data as props — it never fetches data itself
export default function BlogPost({ post }) {
return <h1>{post.title}</h1>;
}
App Router — Fetch directly in the component
// app/blog/[slug]/page.js — App Router data fetching
// No special exported functions. The component IS async.
// Server Components can use async/await directly.
export default async function BlogPost({ params }) {
const { slug } = await params;
// fetch() is extended by Next.js with caching options
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 60 }, // ISR equivalent
});
const post = await res.json();
return <h1>{post.title}</h1>;
}
// For SSG equivalent — tell Next.js which paths to pre-render
export async function generateStaticParams() {
const res = await fetch("https://api.example.com/posts");
const posts = await res.json();
return posts.map((post) => ({ slug: post.slug }));
}
The difference is not cosmetic. In the Pages Router, data fetching is separated from the component — the component receives data as props. In the App Router, data fetching is colocated with the component — the component fetches its own data. This means:
- No prop drilling from page-level data fetching functions to deeply nested components
- Each component can independently fetch exactly the data it needs
- Next.js automatically deduplicates identical fetch requests (if two components fetch the same URL, only one request is made)
The Complete Comparison Table
This table is interview gold. Memorize the key rows.
| Feature | Pages Router (pages/) | App Router (app/) |
|---|---|---|
| Routing | File-based (file = route) | Folder-based (folder + page.js = route) |
| Default component type | Client Component (all JS shipped) | Server Component (zero JS shipped) |
| Layouts | _app.js global + per-page hack | Nested layout.js per folder |
| Data fetching | getStaticProps, getServerSideProps, getStaticPaths | async components, fetch() with cache options, generateStaticParams |
| Loading states | Manual implementation | loading.js (automatic Suspense boundary) |
| Error handling | Manual Error Boundary | error.js (automatic Error Boundary) |
| API routes | pages/api/endpoint.js | app/api/endpoint/route.js |
| Streaming | Not supported | Built-in with Suspense |
| Server Actions | Not supported | "use server" for form handling and mutations |
| React Server Components | Not supported | Default behavior |
| Metadata | next/head component | metadata export or generateMetadata function |
| Middleware | Supported | Supported (same) |
| Stability | Mature, battle-tested (7+ years) | Stable since Next.js 13.4 (2023) |
| Community resources | Massive — most tutorials use Pages Router | Growing rapidly |
Why the App Router Exists — React Server Components
This is the "why" that separates a strong interview answer from a mediocre one.
The App Router was not built because the Pages Router had bad folder structure. It was built because React itself changed.
React Server Components (RSC) introduced a new model:
- Server Components run on the server, render to HTML, and send zero JavaScript to the browser
- Client Components work like traditional React — they hydrate on the client and handle interactivity
The Pages Router cannot support this model. In the Pages Router, every component is a client component. Even if you use getServerSideProps to fetch data on the server, the component itself (with all its JavaScript) is still shipped to the browser for hydration.
The App Router was designed from scratch around this new reality:
- Components are Server Components by default (zero JS)
- You explicitly opt into client-side JavaScript with
"use client" - The result: dramatically smaller bundles for content-heavy pages
Pages Router mental model:
Server (data) -> Client (render + hydrate everything)
App Router mental model:
Server (data + render most things) -> Client (hydrate only interactive parts)
Interview answer template: "The App Router exists because React introduced Server Components, which fundamentally change how rendering works. The Pages Router was built on the assumption that every component eventually runs on the client. The App Router was built on the assumption that most components should run on the server, and only interactive parts should ship JavaScript to the browser."
Migration Considerations
Interviewers love asking: "How would you migrate a Pages Router app to App Router?"
The Incremental Approach
Next.js supports running both routers simultaneously. This is critical to know:
my-app/
app/ # App Router (new pages go here)
dashboard/
page.js
pages/ # Pages Router (existing pages stay here)
about.js
blog/
[slug].js
Rules when both coexist:
- If the same route exists in both
app/andpages/, the App Router takes priority - Middleware works with both routers
- You can migrate page by page — no big-bang rewrite required
What Changes During Migration
| Pages Router Pattern | App Router Equivalent |
|---|---|
getStaticProps | fetch() with { next: { revalidate: N } } in an async component |
getServerSideProps | fetch() with { cache: 'no-store' } in an async component |
getStaticPaths | generateStaticParams |
useRouter() from next/router | useRouter() from next/navigation (different API!) |
next/head for metadata | export const metadata or generateMetadata |
_app.js | app/layout.js (root layout) |
_document.js | app/layout.js (the <html> and <body> tags) |
pages/api/ | app/api/route.js or Server Actions |
The Biggest Migration Trap
// WRONG — using next/router in App Router
import { useRouter } from "next/router"; // This import BREAKS in App Router
// CORRECT — using next/navigation in App Router
import { useRouter } from "next/navigation"; // Different module, different API
// The API is also different:
// Pages Router: router.query.slug
// App Router: params.slug (passed as prop) or useParams()
Which Router Do Interviewers Ask About?
Short answer: Both, but they expect you to know the App Router deeply.
Typical interview scenarios:
- "Explain the difference between App Router and Pages Router" — The comparison table above is your answer
- "How would you fetch data in Next.js?" — Start with App Router (async components), then mention Pages Router equivalents
- "What are React Server Components?" — This is an App Router question. If you answer with
getServerSideProps, you're showing Pages Router knowledge only - "We have a legacy Pages Router app — how would you approach migration?" — Incremental migration, both routers coexisting, page-by-page strategy
Rule of thumb: Default to App Router in your answers. Mention Pages Router when comparing or when discussing migration. Never answer a modern Next.js question with only Pages Router patterns.
Common Mistakes
-
Confusing "Server Component" with "Server-Side Rendering" — SSR (via
getServerSideProps) still ships JavaScript to the client for hydration. Server Components ship ZERO JavaScript. These are different concepts. The App Router uses Server Components by default; the Pages Router uses SSR as an option but every component is still a client component. -
Importing from the wrong router module —
next/routeris for Pages Router.next/navigationis for App Router. Using the wrong one causes runtime errors or silent bugs. In interviews, specifying the correct import signals that you have actually built with both systems. -
Assuming App Router is "just a new folder structure" — The folder change from
pages/toapp/is the least important difference. The real shift is the rendering model: server-first components, nested layouts, streaming, and colocated data fetching. If you describe it as "they just moved files to app/", you are missing the entire architectural point.
Interview Questions
Q: What is the fundamental difference between the Pages Router and the App Router?
The Pages Router treats every component as a client component — all JavaScript is shipped to the browser for hydration. The App Router uses React Server Components by default, meaning most components run on the server and send zero JavaScript to the client. Only components marked with
"use client"ship JavaScript. This is not just a routing change — it is a fundamental shift in how React components are rendered and delivered.
Q: Can you use both pages/ and app/ in the same Next.js project?
Yes. Next.js supports incremental adoption. Both routers can coexist in the same project. If the same route is defined in both, the App Router takes priority. This allows teams to migrate page by page without a full rewrite.
Q: How does data fetching differ between the two routers?
Q: What are the special files in the App Router and what do they do?
Q: Why can't you use useState or useEffect in a Server Component?
Server Components run on the server and produce static HTML. They have no browser lifecycle — there is no DOM, no mount/unmount cycle, no client-side state.
useStatemanages client-side state that persists across re-renders in the browser.useEffectruns side effects after the component mounts in the browser. Neither concept applies to a component that only runs on the server. If you need these hooks, you must add the"use client"directive to make the component a Client Component.
Quick Reference -- Cheat Sheet
+------------------------------------------+-------------------------------------------+
| PAGES ROUTER (pages/) | APP ROUTER (app/) |
+------------------------------------------+-------------------------------------------+
| File = Route | Folder + page.js = Route |
| pages/about.js -> /about | app/about/page.js -> /about |
+------------------------------------------+-------------------------------------------+
| All components are Client Components | Components are Server Components |
| (JS always shipped to browser) | by default (zero JS shipped) |
+------------------------------------------+-------------------------------------------+
| getStaticProps / getServerSideProps | async component + fetch() |
| (data fetching separated from component) | (data fetching colocated in component) |
+------------------------------------------+-------------------------------------------+
| _app.js (single global layout) | layout.js (nested per-folder layouts) |
+------------------------------------------+-------------------------------------------+
| import from 'next/router' | import from 'next/navigation' |
+------------------------------------------+-------------------------------------------+
| Manual loading/error states | loading.js + error.js (automatic) |
+------------------------------------------+-------------------------------------------+
| No streaming | Streaming with Suspense built-in |
+------------------------------------------+-------------------------------------------+
| No Server Actions | "use server" for mutations |
+------------------------------------------+-------------------------------------------+
Interview Default: Answer with App Router. Mention Pages Router for comparison.
Previous: 01-why-nextjs-over-plain-react.md Next: 03-project-structure-and-conventions.md
This is Lesson 1.2 of the Next.js Interview Prep Course -- 8 chapters, 33 lessons.