EthicsComputerCourses
Join training +91 90532 10052

Pages Router (Legacy)

Next.js · Lesson 25 of 27 Advanced 7 min read Updated

The App Router is the future, but thousands of working Next.js sites were built with the older Pages Router, and many companies still maintain them. If you join a team or freelance on an existing project, there is a good chance you will open a pages folder. You need to read this code confidently, fix bugs in it and, eventually, migrate it.

In this lesson you will learn how the Pages Router maps files to URLs, what _app and _document do, the three data-fetching functions, API routes, and the next/router hook. The next lesson shows how to move to the App Router.

Files in pages become routes

In the Pages Router, every file in the pages folder is a route, and its default export is the page component:

Text
pages/
  _app.js              wraps every page
  _document.js         custom <html> and <body>
  index.js             /
  about.js             /about
  courses/
    index.js           /courses
    [slug].js          /courses/react, /courses/html
  docs/[...parts].js   /docs/a, /docs/a/b
  api/
    enquiries.js       /api/enquiries (API route)

Unlike the App Router, any file here becomes public, so components cannot be kept inside pages. Projects usually have a separate components folder.

There are no Server Components in the Pages Router. Every page is rendered to HTML on the server (or at build time) and then fully hydrated in the browser, so all of its JavaScript is sent to the client. The Pages Router uses the React version from your package.json.

_app.js and _document.js

_app.js wraps every page. It is where older apps import global CSS, add shared layout and context providers:

React JSX
// pages/_app.js
import "../styles/globals.css";
import Header from "../components/Header";

export default function MyApp({ Component, pageProps }) {
  return (
    <>
      <Header />
      <Component {...pageProps} />
    </>
  );
}

_document.js customises the outer <html> and <body>, for example to set lang. It renders only on the server and uses special components (Html, Head, Main, NextScript) from next/document. In the App Router, both files are replaced by the root layout.

Data fetching: three special functions

A page can export one of these async functions. Next.js runs it on the server and passes the result to the page as props:

FunctionWhen it runsUse for
getStaticPropsAt build time (and in the background when revalidate is set)Content that is the same for everyone: blog posts, course pages
getStaticPathsAt build time, with getStaticProps, for dynamic routesListing which [slug] pages to prebuild
getServerSidePropsOn every requestData that depends on the request: cookies, search filters
React JSX
// pages/courses/[slug].js
export async function getStaticPaths() {
  const courses = await getCourses();
  return {
    paths: courses.map((c) => ({ params: { slug: c.slug } })),
    fallback: "blocking", // render unknown slugs on first request
  };
}

export async function getStaticProps({ params }) {
  const course = await getCourse(params.slug);
  if (!course) return { notFound: true };
  return { props: { course }, revalidate: 3600 }; // ISR: rebuild at most every hour
}

export default function CoursePage({ course }) {
  return <h1>{course.title}: ₹{course.fee}</h1>;
}

fallback can be false (unknown slugs give 404), true (show a loading version first) or "blocking" (wait for the page to be built). Returning { notFound: true } gives a 404 and { redirect: { destination: "/", permanent: false } } redirects. Note that params here is a plain object, not a Promise.

getServerSideProps receives a context with params, query (the query string), req and res:

React JSX
// pages/search.js
export async function getServerSideProps({ query }) {
  const q = query.q ?? "";
  const results = await searchCourses(q);
  return { props: { q, results } };
}

export default function SearchPage({ q, results }) {
  return (
    <>
      <h1>Results for "{q}"</h1>
      <ul>{results.map((r) => <li key={r.slug}>{r.title}</li>)}</ul>
    </>
  );
}

The pattern is always "a server function returns props, the page renders them". This runnable example imitates that flow with a tiny fake framework, so you can watch the order of events:

React JSXTry it yourself
import { createRoot } from "react-dom/client";

const db = [
  { slug: "html", title: "HTML Basics" },
  { slug: "react", title: "React Basics" },
  { slug: "nextjs", title: "Next.js Basics" },
];

// What a Pages Router file exports
async function getServerSideProps({ query }) {
  console.log("1. getServerSideProps runs on the server with query", JSON.stringify(query));
  const results = db.filter((c) => c.title.toLowerCase().includes((query.q ?? "").toLowerCase()));
  return { props: { q: query.q ?? "", results } };
}

function SearchPage({ q, results }) {
  console.log("2. SearchPage renders with", results.length, "results");
  return (
    <>
      <h1>Results for "{q}"</h1>
      <ul>{results.map((r) => <li key={r.slug}>{r.title}</li>)}</ul>
    </>
  );
}

// What Next.js does for each request
const { props } = await getServerSideProps({ query: { q: "basics" } });
createRoot(document.getElementById("root")).render(<SearchPage {...props} />);
Output
1. getServerSideProps runs on the server with query {"q":"basics"}
2. SearchPage renders with 3 results

API routes

Files in pages/api are API endpoints. Each exports one handler that receives Node.js-style req and res objects:

JavaScript
// pages/api/enquiries.js
export default async function handler(req, res) {
  if (req.method !== "POST") {
    res.setHeader("Allow", "POST");
    return res.status(405).json({ error: "Method not allowed" });
  }
  const { name, phone } = req.body;
  if (!name || !phone) return res.status(400).json({ error: "name and phone required" });
  await saveEnquiry({ name, phone });
  res.status(201).json({ ok: true });
}

In the App Router, the same job is done by a route.js file with one export per method (see Route Handlers), and forms usually use Server Actions instead.

next/router and next/head

The Pages Router's useRouter comes from next/router and includes the query string and dynamic params in router.query:

React JSX
import { useRouter } from "next/router";
import Head from "next/head";

export default function Filters() {
  const router = useRouter();
  const level = router.query.level ?? "all";
  return (
    <>
      <Head><title>Courses: {level}</title></Head>
      <button onClick={() => router.push({ pathname: "/courses", query: { level: "beginner" } })}>
        Beginner
      </button>
    </>
  );
}

Page titles and meta tags are set with the <Head> component from next/head inside each page. <Link> from next/link works in both routers.

Common mistakes

  • Mixing APIs. getServerSideProps does nothing in the app folder, and next/navigation hooks do not belong in pages. Check which folder you are in.
  • Putting helper components inside pages. Every file there becomes a URL. Keep components outside.
  • Returning non-serializable props. Props from getStaticProps and getServerSideProps must be JSON-serializable; convert Date objects to strings.
  • Using getServerSideProps for data that rarely changes. It runs on every request; getStaticProps with revalidate is much cheaper.

Key points

  • In the Pages Router, every file in pages is a route; _app.js wraps pages and _document.js controls <html> and <body>.
  • There are no Server Components; whole pages are hydrated in the browser.
  • getStaticProps (with optional revalidate), getStaticPaths and getServerSideProps load data and return props.
  • API routes in pages/api export one handler(req, res).
  • useRouter comes from next/router, and <Head> from next/head sets metadata.

Exercise

Read this Pages Router page and answer: when does the data load, what happens for a slug not returned by getStaticPaths, and how often can the page update? Then write the URL that shows the second course.

React JSX
export async function getStaticPaths() {
  return { paths: [{ params: { id: "101" } }, { params: { id: "102" } }], fallback: false };
}
export async function getStaticProps({ params }) {
  const res = await fetch(`https://api.example.com/batches/${params.id}`);
  return { props: { batch: await res.json() }, revalidate: 600 };
}
export default function Batch({ batch }) { return <h1>{batch.name}</h1>; }
Show solution
Text
File: pages/batches/[id].js (assumed)
Data loads: at build time for ids 101 and 102, then again in the background at most every 600 seconds (10 minutes) when someone visits.
Unknown id (for example 103): fallback is false, so the visitor gets a 404 page.
Second course URL: /batches/102

revalidate: 600 turns on Incremental Static Regeneration. Because fallback is false, only the two listed ids can ever be shown until the site is rebuilt.

Quick Quiz

1. Which Pages Router function runs on every request?

2. What replaces _app.js and _document.js in the App Router?

3. In getStaticProps, what does returning revalidate: 60 do?

4. Where does the Pages Router's useRouter come from?

Learn Next.js with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call