Skip to content

Next.js Pages Router — Server-Side Rendering with the Next.js Legacy Router

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Next.js Pages Router. We cover key concepts, practical examples, and best practices to help you master this topic.

Next.js Pages Router provides SSR with file-based routing, getServerSideProps for per-request data fetching, automatic code splitting, and seamless client-side navigation for React applications.

What You'll Learn

By the end of this tutorial, you will understand how to build SSR applications with Next.js Pages Router, how file-based routing works, how to fetch data server-side with getServerSideProps, how to handle dynamic routes, and how to deploy Next.js SSR applications.

Why It Matters

Next.js is the most popular React framework, and the Pages Router is its most mature feature. It handles the complex parts of SSR automatically — routing, code splitting, bundling, and hydration — so you can focus on building features instead of configuring Webpack and Express.

Real-World Use

A content platform migrated from Create React App to Next.js Pages Router with getServerSideProps. Time-to-content dropped from 4s to 1.2s. The Migration took two weeks and required no changes to existing React components. Organic traffic increased 80 percent after pages were indexed faster by Google.

Next.js Pages Router SSR Flow
    ┌──────────────────────────────────────────────────────────┐
    │           Next.js Pages Router SSR                       │
    ├──────────────────────────────────────────────────────────┤
    │                                                          │
    │  Request → Server → 1. Match route from /pages           │
    │                      2. Run getServerSideProps           │
    │                      3. Fetch data (API, DB)             │
    │                      4. Pass data as props to page       │
    │                      5. Render page component to HTML    │
    │                      6. Send HTML + serialized data      │
    │                                                          │
    │  Browser → 1. Display HTML immediately                  │
    │            2. Load JavaScript bundle                     │
    │            3. Hydrate page component                     │
    │            4. Page becomes interactive                   │
    │                                                          │
    │  Navigation: Client-side via next/link + next/router    │
    │             No full page reload                          │
    │                                                          │
    │  File → Route mapping:                                   │
    │    /pages/index.js → /                                   │
    │    /pages/products.js → /products                       │
    │    /pages/products/[id].js → /products/:id              │
    │    /pages/blog/[...slug].js → /blog/*                   │
    │                                                          │
    └──────────────────────────────────────────────────────────┘

Think of Next.js Pages Router like a professional kitchen that handles all the prep work. You provide the recipe (React components) and ingredients (data fetching), and Next.js handles the cooking (bundling), plating (SSR), and serving (deployment). The file-based routing is the menu — the name of each dish determines where it appears in the menu.

Basic Pages with getServerSideProps

// pages/index.js — Home page with SSR data fetching
export default function Home({ products, timestamp }) {
    return (
        <div>
            <h1>Product Catalog</h1>
            <p>Generated at: {timestamp}</p>
            <div className="grid">
                {products.map(product => (
                    <div key={product.id}>
                        <h2>{product.name}</h2>
                        <p>${product.price}</p>
                    </div>
                ))}
            </div>
        </div>
    );
}

// This runs on the server for EVERY request
export async function getServerSideProps(context) {
    const { req, res, query, params } = context;

    try {
        // Fetch data from API or database
        const response = await fetch('https://api.example.com/products');
        const products = await response.json();

        return {
            props: {
                products,
                timestamp: new Date().toISOString()
                // These props are serialized and sent to the client
            }
        };
    } catch (error) {
        // Return error state — page component receives it as props
        return {
            props: {
                products: [],
                error: 'Failed to load products'
            }
        };
    }
}

// getServerSideProps receives:
// context.params — route parameters (for dynamic routes)
// context.query — query string parameters
// context.req — HTTP request object
// context.res — HTTP response object
// context.preview — preview mode flag
// context.previewData — preview data
// context.resolvedUrl — resolved URL

Dynamic Routes and API Integration

// pages/products/[id].js — Dynamic product page
export default function ProductPage({ product, notFound }) {
    if (notFound) {
        return <h1>Product not found</h1>;
    }

    return (
        <div>
            <h1>{product.name}</h1>
            <p>{product.description}</p>
            <p>Price: ${product.price}</p>
            <p>Category: {product.category}</p>
        </div>
    );
}

export async function getServerSideProps({ params, req }) {
    const { id } = params;

    // Fetch product from database
    const product = await db.products.findById(id);

    if (!product) {
        return {
            notFound: true,  // Shows 404 page
        };
    }

    // Set custom header (e.g., for caching)
    return {
        props: { product },
        // Optionally set HTTP headers
        headers: {
            'Cache-Control': 'public, s-maxage=10, stale-while-revalidate=59'
        }
    };
}

// pages/api/products.js — API route (runs on server)
export default async function handler(req, res) {
    if (req.method === 'GET') {
        const products = await db.products.findAll();
        res.status(200).json(products);
    } else if (req.method === 'POST') {
        const product = await db.products.create(req.body);
        res.status(201).json(product);
    } else {
        res.setHeader('Allow', ['GET', 'POST']);
        res.status(405).end(`Method ${req.method} Not Allowed`);
    }
}

Client-Side Navigation

// components/Navigation.js
import Link from 'next/link';
import { useRouter } from 'next/router';

export default function Navigation() {
    const router = useRouter();

    return (
        <nav>
            <Link href="/" className={router.pathname === '/' ? 'active' : ''}>
                Home
            </Link>
            <Link href="/products" prefetch={true}>
                Products
            </Link>
            <Link href="/about">
                About
            </Link>

            {/* Programmatic navigation */}
            <button onClick={() => router.push('/contact')}>
                Contact
            </button>

            {/* Dynamic link */}
            <Link href={`/products/${product.slug}`}>
                View Product
            </Link>
        </nav>
    );
}

// prefetch={true} (default for Link in viewport)
// Preloads the page data and JS before user clicks
// Navigation feels instant

Common Mistakes

  1. Using getServerSideProps for data that does not change per request. If the data is the same for all users, use getStaticProps instead. getServerSideProps runs on every request and adds server load.
  2. Not handling errors in getServerSideProps. If data fetching fails, the page crashes. Always wrap in try-catch and return fallback props or notFound: true.
  3. Returning too much data. Props from getServerSideProps are serialized and sent to the client. Large data sets increase page size. Send only what the page needs.
  4. Using router events for critical logic. Router events can be unreliable. Use getServerSideProps for critical data fetching and validation.
  5. Not Caching SSR pages. Without cache headers, every request hits the server. Set appropriate Cache-Control headers for public pages.

Practice Questions

  1. How does file-based routing work in Next.js Pages Router?
  2. What is the difference between getServerSideProps and getStaticProps?
  3. How do you handle 404 errors in getServerSideProps?
  4. How does client-side navigation work with next/link?
  5. How do you set cache headers for SSR pages in Next.js?

Challenge: Build a product catalog with Next.js Pages Router: home page with getServerSideProps fetching products from an API, dynamic product pages with [id].js, API route for product data, client-side navigation with Link and prefetch, error handling for missing products, and cache headers for public pages.

FAQ

What is the difference between Pages Router and App Router?

Pages Router uses pages/ directory with getServerSideProps. App Router uses app/ directory with React Server Components, Server Actions, and a different data fetching model.

Can I mix getServerSideProps and getStaticProps?

No. A page can only use one data fetching method. Choose based on whether the data changes per request (SSR) or is the same for all users (SSG).

How do I handle authentication in getServerSideProps?

Read the session cookie from context.req, verify the authentication, and redirect to login if unauthenticated using context.res.writeHead or the redirect property.

Does getServerSideProps affect performance?

Yes. Each request triggers server-side data fetching and rendering. Use caching, CDN, and consider ISR for pages that do not need fresh data on every request.

Can I use getServerSideProps with static export?

No. getServerSideProps requires a Node.js server. For static export, use getStaticProps. Next.js handles this automatically during build.

Mini Project

Build a blog with Next.js Pages Router: home page listing posts with getServerSideProps, dynamic post pages ([slug].js) with full content, a search page with query parameters, API routes for post CRUD operations, client-side navigation with Link prefetching, 404 handling, and cache headers for 60-second CDN caching.

What's Next

You understand Pages Router. Now explore Next.js App Router for the modern React Server Components approach.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro