4. Pages Router (Legacy, Still Asked)
The Pages Router is in maintenance mode — the App Router is the default for new projects. But most companies with a Next.js codebase older than 2023 still run it, so interviewers ask about it and about migrating away from it.
1. File Structure
pages/
├── _app.tsx → wraps every page
├── _document.tsx → custom <html> / <body>
├── index.tsx → /
├── about.tsx → /about
├── 404.tsx → custom not found
├── blog/
│ ├── index.tsx → /blog
│ └── [slug].tsx → /blog/:slug
└── api/
└── users.ts → /api/usersDynamic Segments
| File | Matches |
|---|---|
[id].tsx | /posts/1 |
[...slug].tsx | /shop/a/b/c |
[[...slug]].tsx | /shop and /shop/a/b |
2. The Three Data Fetching Functions
The core of every Pages Router interview question.
getStaticProps — SSG
Runs at build time. The page becomes static HTML.
export async function getStaticProps() {
const posts = await getPosts();
return {
props: { posts },
revalidate: 60, // ISR: regenerate at most once per 60s
};
}
export default function Blog({ posts }) {
return <PostList posts={posts} />;
}Return { notFound: true } to render the 404 page, or { redirect: { destination, permanent } } to redirect.
getStaticPaths — Which Dynamic Pages To Prebuild
export async function getStaticPaths() {
const posts = await getPosts();
return {
paths: posts.map((p) => ({ params: { slug: p.slug } })),
fallback: "blocking",
};
}The fallback Values
| Value | Behaviour for an unlisted path |
|---|---|
false | 404 |
true | Serve a fallback page immediately, generate in the background, then swap in |
"blocking" | SSR it on first request, cache the result, no fallback flash |
With fallback: true you must handle the loading state:
const router = useRouter();
if (router.isFallback) return <Skeleton />;getServerSideProps — SSR
Runs on every request.
export async function getServerSideProps(context) {
const { params, query, req, res } = context;
const session = await getSession(req);
if (!session) {
return { redirect: { destination: "/login", permanent: false } };
}
const data = await getDashboard(session.userId);
return { props: { data } };
}Comparison
getStaticProps | getServerSideProps | |
|---|---|---|
| Runs | Build time (+ ISR) | Every request |
| Output | Static HTML on CDN | HTML generated per request |
| TTFB | Fastest | Slower |
| Access to request | No | Yes (req, res, cookies) |
| Use for | Blogs, docs, marketing | Personalised, auth-gated pages |
Interview Point
All three run server-side only. Their code is stripped from the client bundle, so it is safe to use database clients and secrets inside them. That is why getServerSideProps can call your ORM directly.
3. Incremental Static Regeneration
export async function getStaticProps() {
return { props: { posts }, revalidate: 60 };
}How it works:
- First request after the window: the stale page is served immediately
- Next.js regenerates the page in the background
- Subsequent requests get the fresh version
This is stale-while-revalidate. The user never waits for the rebuild.
On-Demand ISR
// pages/api/revalidate.ts
export default async function handler(req, res) {
if (req.query.secret !== process.env.REVALIDATE_SECRET) {
return res.status(401).json({ message: "Invalid token" });
}
await res.revalidate("/blog/my-post");
return res.json({ revalidated: true });
}Call this from your CMS webhook when content is published.
4. _app.tsx and _document.tsx
_app.tsx
Wraps every page. Use it for global CSS, providers and persistent layout.
export default function App({ Component, pageProps }) {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
</QueryClientProvider>
);
}_document.tsx
Customises the HTML shell. Rendered once on the server — never in the browser.
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body className="antialiased">
<Main />
<NextScript />
</body>
</Html>
);
}Interview Point
_app vs _document: _app runs on both server and client and is where React state and providers live. _document runs only on the server and only exists to control the surrounding HTML. Never put event handlers or hooks in _document.
5. Routing
import { useRouter } from "next/router"; // note: next/router, not next/navigation
const router = useRouter();
router.push("/dashboard");
router.replace("/login");
router.back();
router.query; // { slug: "hello", page: "2" }
router.pathname; // "/blog/[slug]"
router.asPath; // "/blog/hello?page=2"
router.isReady; // query is only populated after this is true
router.isFallback; // true while a fallback page is generatingThe isReady Gotcha
On a statically generated page, router.query is an empty object on the first render. Reading it too early gives undefined.
useEffect(() => {
if (!router.isReady) return;
fetchData(router.query.id);
}, [router.isReady, router.query.id]);6. Per-Page Layouts
The Pages Router has no nested layouts, so the community pattern is:
// pages/dashboard.tsx
Dashboard.getLayout = (page) => <DashboardLayout>{page}</DashboardLayout>;
// pages/_app.tsx
export default function App({ Component, pageProps }) {
const getLayout = Component.getLayout ?? ((page) => page);
return getLayout(<Component {...pageProps} />);
}The App Router replaced all of this with nested layout.tsx files — one of the strongest arguments for migrating.
7. Pages Router vs App Router
| Pages Router | App Router | |
|---|---|---|
| Directory | pages/ | app/ |
| Default component type | Client | Server |
| Data fetching | getStaticProps / getServerSideProps | async components, "use cache" |
| Nested layouts | Manual getLayout | layout.tsx |
| Loading state | Manual | loading.tsx |
| Error handling | _error.tsx | error.tsx per segment |
| Streaming | No | Yes |
| Server Actions | No | Yes |
| Router hook | next/router | next/navigation |
| Route params | router.query | await params |
| API | pages/api/* | app/**/route.ts |
| Status | Maintenance mode | Active development |
8. Migration Strategy
Both routers can coexist in one project — app/ takes precedence for conflicting paths. That makes an incremental migration possible.
Order That Works
- Add the
app/directory alongsidepages/ - Move the least risky route first (a static marketing page)
- Create
app/layout.tsxmirroring your_appproviders - Convert
getServerSideProps→asyncServer Component - Convert
getStaticProps→"use cache"(Next 16) orgenerateStaticParams+ caching - Swap
next/routerfornext/navigationandrouter.queryforawait params - Add
"use client"only where hooks or handlers are actually used - Move
pages/api/*toapp/**/route.tslast
Common Migration Bugs
| Bug | Cause |
|---|---|
useRouter is not a function | Still importing from next/router |
params.slug is undefined | Forgot await params |
useState is not defined | Missing "use client" |
| Entire app became client-side | "use client" in the root layout |
| CSS-in-JS broke | Runtime CSS-in-JS is not supported in Server Components |
window is not defined | Browser API in a Server Component |
Interview Point
If asked "would you migrate an existing Pages Router app?" — the honest answer is: incrementally, starting with pages that benefit most from server rendering or nested layouts, and not at all if the app is stable and rarely touched. A rewrite with no user-visible benefit is hard to justify. Interviewers respect that answer more than "yes, App Router is better".