Skip to content

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/users

Dynamic Segments

FileMatches
[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.

tsx
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

tsx
export async function getStaticPaths() {
  const posts = await getPosts();

  return {
    paths: posts.map((p) => ({ params: { slug: p.slug } })),
    fallback: "blocking",
  };
}

The fallback Values

ValueBehaviour for an unlisted path
false404
trueServe 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:

tsx
const router = useRouter();
if (router.isFallback) return <Skeleton />;

getServerSideProps — SSR

Runs on every request.

tsx
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

getStaticPropsgetServerSideProps
RunsBuild time (+ ISR)Every request
OutputStatic HTML on CDNHTML generated per request
TTFBFastestSlower
Access to requestNoYes (req, res, cookies)
Use forBlogs, docs, marketingPersonalised, 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

tsx
export async function getStaticProps() {
  return { props: { posts }, revalidate: 60 };
}

How it works:

  1. First request after the window: the stale page is served immediately
  2. Next.js regenerates the page in the background
  3. Subsequent requests get the fresh version

This is stale-while-revalidate. The user never waits for the rebuild.

On-Demand ISR

ts
// 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.

tsx
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.

tsx
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

tsx
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 generating

The isReady Gotcha

On a statically generated page, router.query is an empty object on the first render. Reading it too early gives undefined.

tsx
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:

tsx
// 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 RouterApp Router
Directorypages/app/
Default component typeClientServer
Data fetchinggetStaticProps / getServerSidePropsasync components, "use cache"
Nested layoutsManual getLayoutlayout.tsx
Loading stateManualloading.tsx
Error handling_error.tsxerror.tsx per segment
StreamingNoYes
Server ActionsNoYes
Router hooknext/routernext/navigation
Route paramsrouter.queryawait params
APIpages/api/*app/**/route.ts
StatusMaintenance modeActive 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

  1. Add the app/ directory alongside pages/
  2. Move the least risky route first (a static marketing page)
  3. Create app/layout.tsx mirroring your _app providers
  4. Convert getServerSidePropsasync Server Component
  5. Convert getStaticProps"use cache" (Next 16) or generateStaticParams + caching
  6. Swap next/router for next/navigation and router.query for await params
  7. Add "use client" only where hooks or handlers are actually used
  8. Move pages/api/* to app/**/route.ts last

Common Migration Bugs

BugCause
useRouter is not a functionStill importing from next/router
params.slug is undefinedForgot await params
useState is not definedMissing "use client"
Entire app became client-side"use client" in the root layout
CSS-in-JS brokeRuntime CSS-in-JS is not supported in Server Components
window is not definedBrowser 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".

© 2025 DDocs · Dipak's Documentation Guide