Skip to content
Home JavaScript Next.js Basics Explained: Pages, Routing, and Data Fetching

Next.js Basics Explained: Pages, Routing, and Data Fetching

Where developers are forged. · Structured learning · Free forever.
📍 Part of: React.js → Topic 13 of 47
Next.
⚙️ Intermediate — basic JavaScript knowledge assumed
In this tutorial, you'll learn
Next.
  • File-based routing means your folder structure in pages/ or app/ IS your navigation — no router config to maintain
  • SSG (getStaticProps) builds HTML at deploy time and serves from CDN — always default to this
  • ISR (adding revalidate: N to getStaticProps) gives you the speed of static with near-real-time freshness
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • Next.js is a React framework that adds file-based routing, server rendering, and build-time optimisation
  • File-based routing: drop a file in pages/ or app/ and the route is live — no router config needed
  • SSG (getStaticProps) builds HTML at deploy time — fastest option, served from CDN
  • SSR (getServerSideProps) builds HTML per request — use only when data must be fresh AND in initial HTML
  • ISR adds a revalidate timer to SSG — static speed with near-real-time freshness
  • Biggest mistake: using SSR everywhere for 'freshness' when ISR solves 90% of those cases faster
🚨 START HERE
Next.js Quick Debug Reference
Fast commands for diagnosing Next.js rendering and build issues
🟡Page is server-rendered instead of static
Immediate ActionCheck build output for rendering mode
Commands
next build 2>&1 | grep -E '●|λ|○'
cat .next/server/pages/products/*.html 2>/dev/null | head -5
Fix NowRemove getServerSideProps and use getStaticProps — or check if dynamic API usage (cookies, headers) forces SSR
🟡Build fails with getStaticPaths error
Immediate ActionCheck that getStaticPaths returns correct shape
Commands
grep -rn 'getStaticPaths' pages/ --include='*.js' --include='*.tsx'
next build 2>&1 | grep -A5 'getStaticPaths'
Fix NowEnsure paths array items have { params: { key: value } } matching the dynamic route filename
🟡Image optimization not working in production
Immediate ActionCheck next.config.js for image domains
Commands
cat next.config.js | grep -A10 'images'
curl -sI http://localhost:3000/_next/image?url=/test.jpg | head -10
Fix NowAdd remote image domains to next.config.js images.domains or images.remotePatterns
🟡Hydration mismatch error in console
Immediate ActionFind components that render differently on server vs client
Commands
grep -rn 'typeof window\|Date.now\|Math.random' pages/ components/ --include='*.js' --include='*.tsx'
grep -rn 'useEffect\|useState' pages/ --include='*.js' --include='*.tsx'
Fix NowMove browser-dependent logic into useEffect or use dynamic import with ssr: false
Production IncidentgetServerSideProps on Every Page Crashed the Server Under Black Friday TrafficAn e-commerce site used getServerSideProps on all 45,000 product pages. On Black Friday, the server could not handle the load and response times exceeded 30 seconds.
SymptomAverage response time jumped from 120ms to 8,400 CPU hit 100% and started dropping connections. Users saw timeouts and 502 errors. Revenue dropped 40% in the first hour.
AssumptiongetServerSideProps was used on product pages because the team wanted 'fresh' pricing data on every request.
Root causeEvery product page used getServerSideProps to fetch pricing from the database on every request. Product prices changed once per day at most. The team chose SSR because they wanted to avoid stale pricing, but ISR with a 60-second revalidate would have served the same freshness with 99% less server load. Under normal traffic (500 req/s), the server handled SSR fine. Under Black Friday traffic (12,000 req/s), the server could not generate 12,000 HTML pages per second — each page took 50-200ms to render, and the server had 4 cores.
FixConverted product pages from getServerSideProps to getStaticProps with revalidate: 60. Pre-built the top 10,000 products at deploy time. Less popular products used fallback: 'blocking' to SSR on first hit, then cache. Added an on-demand revalidation webhook that triggers when prices change in the CMS. Response time dropped to 15ms for cached pages. Server CPU dropped to 15% under the same traffic.
Key Lesson
getServerSideProps runs a server function on every request — it is the most expensive rendering strategyISR with revalidate: N serves cached pages and regenerates in the background — use it instead of SSR for data that changes infrequentlyPre-build your most popular pages at deploy time — they serve instantly from CDNUse on-demand revalidation (res.revalidate) to invalidate specific pages when data changes — do not rely on time-based revalidation alone
Production Debug GuideDiagnose rendering and data fetching issues in production
Page loads slowly despite using getStaticPropsCheck if the page is actually being served statically — run next build and verify the page appears as ● (static) not λms during peak traffic. The server's (server)
router.query is empty on first renderThis is expected — router.query is empty during hydration. Add a loading state or use getServerSideProps to pass the query as props
ISR page shows stale data after CMS updateUse on-demand revalidation with res.revalidate('/path') triggered by a CMS webhook — do not rely on time-based revalidation alone
getStaticPaths with fallback: false returns 404 for valid pagesSet fallback: 'blocking' to SSR unknown pages on first hit, then cache them — or pre-build all valid paths
App Router Server Component throws error when using useStateServer Components cannot use React hooks — add 'use client' directive to the file or move the interactive part to a separate client component

Next.js solves three problems that plain React cannot: initial page load performance, search engine visibility, and routing complexity. A standard React app ships a blank HTML shell and waits for JavaScript to hydrate — users see a spinner, crawlers see nothing. Next.js renders pages on the server or at build time, so users and search engines get real HTML immediately.

The framework supports four rendering strategies in a single project: Static Site Generation (SSG), Server-Side Rendering (SSR), Client-Side Rendering (CSR), and Incremental Static Regeneration (ISR). Choosing the right strategy per page is the core skill that separates a competent Next.js developer from one who ships slow, expensive applications.

File-Based Routing: Why Your Folder Structure IS Your App's Navigation

In a standard React app, you manually wire up React Router — you import BrowserRouter, define Route components, and manage path strings in your head. It works, but it's boilerplate you didn't need to write. Next.js takes a different philosophy: your file system is your router. Drop a file called about.js inside the pages folder and /about becomes a live route instantly. No imports. No configuration. No forgetting to register a new page.

This isn't just a convenience trick. It's an architectural choice that enforces consistency across teams. When a new developer joins your project, they don't need to read routing config — they just look at the folder structure. Files are truth.

Dynamic routes use square-bracket syntax. A file named [productId].js matches /products/42, /products/shoes, or any slug you throw at it. The matched value lands in useRouter().query so you can fetch the right data. This pattern covers 90% of real-world routing needs — product pages, blog posts, user profiles — without a single line of router config.

ProductPage.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041
// File location: pages/products/[productId].js
// This single file handles EVERY product URL: /products/1, /products/42, etc.

import { useRouter } from 'next/router';

export default function ProductPage() {
  // useRouter gives us access to the URL parameters
  const router = useRouter();

  // productId comes from the filename [productId].js
  // On the URL /products/42, this will be the string '42'
  const { productId } = router.query;

  // During the initial render on the client, query can be empty
  // so we guard against that with a loading state
  if (!productId) {
    return <p>Loading product...</p>;
  }

  return (
    <div>
      <h1>Product Details</h1>
      {/* Shows the dynamic segment from the URL */}
      <p>You are viewing product ID: {productId}</p>
      <p>
        This page was generated by the file:
        pages/products/[productId].js
      </p>
    </div>
  );
}

// --- FOLDER STRUCTURE REFERENCE ---
// pages/
//   index.js          → renders at /
//   about.js          → renders at /about
//   products/
//     index.js        → renders at /products
//     [productId].js  → renders at /products/:productId
//   blog/
//     [slug].js       → renders at /blog/:slug
⚠ Watch Out: router.query is empty on first render
When Next.js hydrates a page on the client, router.query starts as an empty object {} before populating with the actual URL values. Always guard with if (!productId) return null or a loading state — otherwise you'll pass undefined to your API calls and wonder why nothing works.
📊 Production Insight
router.query is empty during hydration — the first client render has no URL data.
API calls using router.query during the first render send undefined as the ID.
Rule: guard router.query with a loading state — never use it unconditionally.
🎯 Key Takeaway
File-based routing means your folder structure IS your navigation — no router config to maintain.
Dynamic routes use [param].js syntax — the matched value lands in router.query.
router.query is empty on first render — always guard with a loading state.

SSG vs SSR vs CSR: Choosing the Right Data Fetching Strategy

This is where Next.js earns its keep — and where most developers get confused because they treat all three strategies as interchangeable. They're not. Each one answers a different question about WHEN data should be fetched and WHO should do the fetching.

Static Site Generation (SSG) with getStaticProps runs at build time on the server. The HTML is generated once and served from a CDN forever. This is perfect for content that doesn't change often — a product catalogue, a blog post, a pricing page. It's the fastest option because there's no server work happening per request.

Server-Side Rendering (SSR) with getServerSideProps runs on every single request. The server fetches fresh data and builds the HTML before sending it to the user. Use this only when the data must be up-to-the-second fresh AND you need it in the initial HTML — think a live dashboard or a personalised feed that depends on the user's session cookie.

Client-Side Rendering (CSR) is just regular React — the page loads, then JavaScript fetches data in a useEffect. Use this for data that's behind authentication and doesn't need to be indexed by search engines, like user account settings. Mixing all three in the same app based on each page's needs is the Next.js superpower.

BlogPost.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
// File: pages/blog/[slug].js
// This page uses SSG — it's pre-built at deploy time
// Perfect for blog posts that don't change every minute

export default function BlogPost({ post }) {
  // 'post' comes from getStaticProps below — it's already resolved
  // by the time this component renders. No loading state needed!
  return (
    <article>
      <h1>{post.title}</h1>
      <p>By {post.author} on {post.publishedDate}</p>
      <div>{post.content}</div>
    </article>
  );
}

// getStaticPaths tells Next.js WHICH URLs to pre-build
// This runs at build time — not on every request
export async function getStaticPaths() {
  // In real life, fetch this list from your CMS or database
  const publishedSlugs = ['intro-to-nextjs', 'react-hooks-explained', 'css-grid-guide'];

  const paths = publishedSlugs.map((slug) => ({
    params: { slug }, // must match the [slug] in your filename
  }));

  return {
    paths,
    // fallback: false means unknown slugs return a 404
    // fallback: 'blocking' means unknown slugs trigger SSR on first hit, then cache
    fallback: false,
  };
}

// getStaticProps receives the slug and fetches that post's data
// Runs ONCE per slug at build time — never again (until revalidate triggers)
export async function getStaticProps({ params }) {
  // Simulating a CMS or database fetch using the slug from the URL
  const response = await fetch(`https://api.example.com/posts/${params.slug}`);
  const post = await response.json();

  return {
    props: { post }, // injected as props into BlogPost component above
    // revalidate: 60 means re-generate this page in the background
    // if a request comes in after 60 seconds — this is called ISR
    revalidate: 60,
  };
}

// --- WHEN TO USE WHAT ---
// getStaticProps    → blog posts, product pages, marketing pages (fast, SEO-friendly)
// getServerSideProps → user dashboards, pages needing live data or session cookies
// useEffect fetch   → post-login UI, private account pages, non-SEO content
💡Pro Tip: ISR is the sweet spot for most real apps
Incremental Static Regeneration (ISR) — enabled by adding revalidate: N to your getStaticProps return — gives you static speed with near-real-time freshness. The page serves from cache until N seconds pass, then regenerates in the background on the next visit. Most e-commerce and content sites should default to ISR over full SSR.
📊 Production Insight
getServerSideProps runs a server function on every request — the most expensive strategy.
ISR with revalidate: N serves cached pages and regenerates in the background.
Rule: default to SSG/ISR — only use SSR when data must be fresh per-request AND in initial HTML.
🎯 Key Takeaway
SSG builds HTML at deploy time — fastest, served from CDN, no server work per request.
SSR builds HTML per request — expensive, use only when data must be fresh and in initial HTML.
ISR is the sweet spot — static speed with near-real-time freshness via revalidate timer.

Two components trip up almost every developer coming from plain React: next/link and next/image. They look like simple wrappers around an anchor tag and an img tag, but they're doing serious work under the hood — work that would take hours to implement manually.

next/link enables client-side navigation. When you use it, clicking a link doesn't trigger a full browser reload — Next.js swaps the page in JavaScript, keeping your app state alive. But here's the part that actually matters: Next.js automatically prefetches linked pages in the background when they appear in the viewport. The user moves their mouse toward a link and the destination is already loading. This is why Next.js apps feel snappy — it's not magic, it's strategic prefetching.

next/image solves one of the biggest real-world performance problems: images. It automatically resizes images to the right dimensions for the user's device, serves modern formats like WebP when the browser supports it, lazy-loads images below the fold by default, and prevents layout shift by requiring width and height props. The priority prop tells it to load a specific image eagerly — use that on your above-the-fold hero image to nail your Core Web Vitals score.

ProductCard.js · JAVASCRIPT
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// File: components/ProductCard.js
// Demonstrates real-world usage of next/link and next/image

import Link from 'next/link';
import Image from 'next/image';

export default function ProductCard({ product }) {
  return (
    <div className="product-card">
      {/*
        next/image vs plain <img>:
        - Automatically serves WebP format if browser supports it
        - Resizes to the rendered size — not the original 4000px source
        - Lazy loads by default (doesn't load until near viewport)
        - Prevents CLS (Cumulative Layout Shift) via the width/height aspect ratio
      */}
      <Image
        src={product.imageUrl}
        alt={product.name}       // alt is required — good for accessibility AND SEO
        width={400}              // intrinsic width of the image
        height={300}             // intrinsic height — sets the aspect ratio
        // Add priority={true} ONLY for above-the-fold images like hero banners
        // priority={true} tells Next.js to preload it, skipping lazy loading
      />

      <h2>{product.name}</h2>
      <p>${product.price}</p>

      {/*
        next/link vs plain <a>:
        - Client-side navigation — no full page reload
        - Prefetches the linked page HTML+JS in the background when visible
        - Falls back to normal <a> behaviour if JavaScript hasn't loaded yet
      */}
      <Link href={`/products/${product.id}`}>
        {/* As of Next.js 13+, you don't need a nested <a> tag anymore */}
        View Product Details
      </Link>
    </div>
  );
}

// Usage in a parent page:
// <ProductCard
//   product={{
//     id: 42,
//     name: 'Trail Running Shoes',
//     price: 129.99,
//     imageUrl: '/images/trail-shoes.jpg'
//   }}
// />
🔥Interview Gold: Why does next/image require width and height?
It's not just about layout — it's about Cumulative Layout Shift (CLS), a Core Web Vitals metric. Without explicit dimensions, the browser doesn't know how much space to reserve for an image before it loads, causing content to jump around as images pop in. Providing width and height lets the browser pre-allocate that space, which keeps your CLS score near zero and directly impacts your Google search ranking.
📊 Production Insight
next/image prevents CLS by reserving space before the image loads — width/height are required.
Without dimensions, images cause layout shift — Google penalises CLS in search rankings.
Rule: always provide width and height to next/image — it is a Core Web Vitals requirement.
🎯 Key Takeaway
next/link prefetches pages in the background when they enter the viewport — this is why Next.js feels fast.
next/image serves WebP, resizes automatically, and prevents CLS via required width/height.
Use priority={true} only for above-the-fold images — it skips lazy loading for critical content.

The App Router vs Pages Router: What Changed in Next.js 13+

If you've been reading Next.js docs and feeling confused by two completely different patterns, here's the honest explanation: Next.js 13 introduced a brand-new routing system called the App Router, and both systems now coexist in the same framework. The older system is the Pages Router (everything inside a pages/ folder, which is what this article has covered so far). The App Router lives inside an app/ folder and uses a different set of conventions.

The App Router is built around React Server Components — a newer React feature that lets components run only on the server, with zero JavaScript shipped to the browser. In the App Router, every component is a Server Component by default. You opt into client-side interactivity by adding 'use client' at the top of a file.

The data fetching story also changes: instead of getStaticProps and getServerSideProps, you just use async/await directly inside Server Components and call fetch() with caching options. It's more flexible but requires a mental shift.

For new production projects starting today, learn the App Router. For jobs and existing codebases, the Pages Router is still everywhere — understanding both is what separates a competent Next.js developer from a great one.

ProductPageAppRouter.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
// File: app/products/[productId]/page.js  (App Router convention)
// Notice: no getStaticProps, no useRouter for data — just async/await directly

// This is a SERVER COMPONENT by default.
// It runs only on the server. Zero JS is sent to the browser for this component.
// That means: faster page loads, direct database access, no API layer needed.

export default async function ProductPage({ params }) {
  // params comes from the folder name [productId] — same concept as Pages Router
  const { productId } = params;

  // fetch() in Server Components is enhanced by Next.js
  // next: { revalidate: 3600 } is equivalent to ISR — cache for 1 hour
  const response = await fetch(
    `https://api.example.com/products/${productId}`,
    { next: { revalidate: 3600 } } // ISR-style caching built right into fetch
  );

  const product = await response.json();

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

      {/*
        AddToCartButton needs onClick — that's client-side behaviour.
        We mark it 'use client' in its own file and import it here.
        The Server Component passes data down; the Client Component handles interaction.
      */}
      <AddToCartButton productId={productId} />
    </div>
  );
}

// --- app/components/AddToCartButton.js ---
// 'use client' at the top opts this file into client-side rendering
// Only components that NEED interactivity should be client components

'use client';

import { useState } from 'react';

export function AddToCartButton({ productId }) {
  const [added, setAdded] = useState(false);

  function handleClick() {
    // In real life: call your cart API here
    console.log(`Adding product ${productId} to cart`);
    setAdded(true);
  }

  return (
    <button onClick={handleClick}>
      {added ? 'Added to Cart ✓' : 'Add to Cart'}
    </button>
  );
}
💡Pro Tip: Don't reach for 'use client' by default
New App Router developers instinctively add 'use client' to every component because it feels familiar. Resist this. Keep components as Server Components unless they specifically need useState, useEffect, browser APIs, or event handlers. The more Server Components you have, the less JavaScript your users download — and that's the whole point of the App Router.
📊 Production Insight
Adding 'use client' to every component defeats the App Router purpose — you ship more JavaScript.
Server Components run only on the server — zero JS shipped for those components.
Rule: only add 'use client' when a component needs useState, useEffect, or event handlers.
🎯 Key Takeaway
App Router uses Server Components by default — zero JS shipped for server-only components.
Data fetching uses async/await directly in components — no getStaticProps or getServerSideProps.
Only add 'use client' when a component needs browser interactivity — resist the instinct to add it everywhere.

API Routes and Middleware: Building the Backend Inside Next.js

Next.js includes a built-in API layer. Files inside pages/api/ become serverless API endpoints — no separate backend server needed. This is the simplest way to add server-side logic to a Next.js application: form submissions, webhook handlers, authentication endpoints, and database queries all live inside the same project.

The App Router changes the API story. Instead of pages/api/, API routes live inside app/api/ as route.js files. Each HTTP method (GET, POST, PUT, DELETE) is exported as a named function. The handler receives a standard Web Request object and returns a standard Web Response — aligning with the web platform instead of Node.js-specific APIs.

Middleware runs before every request reaches your pages or API routes. It lives in middleware.js at the project root and is the right place for authentication checks, redirects, feature flags, and request logging. Middleware runs on the Edge Runtime by default — it executes at the CDN edge, not on your origin server, which means sub-millisecond latency for simple checks.

route.js · JAVASCRIPT
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
// --- App Router API Route ---
// File: app/api/users/route.js
// Each HTTP method is a named export

import { NextResponse } from 'next/server';

// GET /api/users — list all users
export async function GET(request) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get('page') || '1');
  const limit = parseInt(searchParams.get('limit') || '20');

  // In production: call your database here
  const users = await db.users.findMany({
    skip: (page - 1) * limit,
    take: limit,
  });

  return NextResponse.json({
    data: users,
    pagination: { page, limit },
  });
}

// POST /api/users — create a new user
export async function POST(request) {
  const body = await request.json();

  // Validate input
  if (!body.email || !body.name) {
    return NextResponse.json(
      { error: 'Email and name are required' },
      { status: 400 }
    );
  }

  const user = await db.users.create({ data: body });

  return NextResponse.json({ data: user }, { status: 201 });
}

// --- Middleware ---
// File: middleware.js (project root)
// Runs on every request before pages/API routes

import { NextResponse } from 'next/server';

export function middleware(request) {
  // Example: redirect unauthenticated users
  const token = request.cookies.get('auth-token');

  // Protect dashboard routes
  if (request.nextUrl.pathname.startsWith('/dashboard') && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // Add request ID for tracing
  const response = NextResponse.next();
  response.headers.set('x-request-id', crypto.randomUUID());

  return response;
}

// Matcher controls which routes trigger middleware
export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};
Mental Model
Middleware as a Gatekeeper
Middleware runs before every matched request — it is the gatekeeper that decides whether a request proceeds, redirects, or is rejected.
  • Runs on the Edge Runtime — executes at the CDN, not your origin server
  • Sub-millisecond latency for simple checks like auth tokens and feature flags
  • Cannot use Node.js APIs (fs, crypto.randomBytes) — only Web APIs available
  • Use matcher config to limit which routes trigger middleware — avoid running on static assets
  • The right place for: auth checks, redirects, A/B testing, request logging, rate limiting
📊 Production Insight
Middleware runs on the Edge Runtime — no Node.js APIs available (no fs, no crypto.randomBytes).
Middleware on every request adds latency — use matcher to limit which routes trigger it.
Rule: use middleware for auth and redirects — put business logic in API routes.
🎯 Key Takeaway
API routes turn Next.js into a full-stack framework — no separate backend needed.
App Router API routes use named exports (GET, POST) with standard Web Request/Response.
Middleware runs at the CDN edge — sub-millisecond latency for auth checks and redirects.

Deployment and Performance: What Matters in Production

Understanding rendering strategies is only half the picture. In production, deployment configuration, caching headers, and build optimisation determine whether your app is fast or expensive. A perfectly coded Next.js app deployed without CDN caching is slower than a poorly coded app deployed with proper caching.

Vercel is the default deployment platform for Next.js — both are made by the same company. It handles ISR caching, edge functions, image optimisation, and preview deployments automatically. But Next.js deploys anywhere: AWS, Google Cloud, Docker containers, or static hosting for fully-static sites.

The three production levers: CDN caching (serve static assets from edge locations), ISR revalidation (serve cached pages, regenerate in background), and bundle analysis (ship less JavaScript to the browser). Pulling these three levers correctly is worth more than any code optimisation.

next.config.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
// File: next.config.js
// Production configuration that actually matters

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Image optimisation domains — required for next/image with remote images
  images: {
    domains: ['images.example.com', 'cdn.example.com'],
    // Or use remotePatterns for more control (Next.js 13+)
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**.example.com',
      },
    ],
  },

  // Headers: set caching policies for static assets
  async headers() {
    return [
      {
        // Cache static assets (JS, CSS, images) for 1 year
        source: '/_next/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        // Cache API responses for 60 seconds
        source: '/api/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, s-maxage=60, stale-while-revalidate=300',
          },
        ],
      },
    ];
  },

  // Redirects: handle legacy URLs and SEO
  async redirects() {
    return [
      {
        source: '/old-blog/:slug',
        destination: '/blog/:slug',
        permanent: true, // 301 redirect — SEO-friendly
      },
    ];
  },

  // Rewrites: proxy external APIs through Next.js
  async rewrites() {
    return [
      {
        source: '/api/external/:path*',
        destination: 'https://external-api.example.com/:path*',
      },
    ];
  },

  // Bundle analysis — run ANALYZE=true next build to visualise
  // Identifies large dependencies that should be code-split
  ...(process.env.ANALYZE === 'true' && {
    webpack: (config) => {
      const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
      config.plugins.push(
        new BundleAnalyzerPlugin({
          analyzerMode: 'static',
          reportFilename: './bundle-analysis.html',
        })
      );
      return config;
    },
  }),
};

module.exports = nextConfig;

// --- Production Checklist ---
//
// 1. Run ANALYZE=true next build — check bundle-analysis.html for large deps
// 2. Set Cache-Control headers for static assets (1 year, immutable)
// 3. Set Cache-Control for API routes (s-maxage + stale-while-revalidate)
// 4. Use next/image for all images — never use plain <img>
// 5. Use next/link for all internal navigation — never use plain <a>
// 6. Pre-render top pages with getStaticProps — serve from CDN
// 7. Use ISR for pages that change infrequently — not full SSR
// 8. Add middleware for auth — do not check auth in every page component
⚠ The Three Most Expensive Mistakes in Production
📊 Production Insight
No Cache-Control headers means every request hits your server — even for unchanged assets.
ISR serves cached pages and regenerates in background — 95% reduction in server compute.
Rule: set Cache-Control headers, use ISR over SSR, and optimise images with next/image.
🎯 Key Takeaway
Deployment configuration matters as much as code quality — caching headers and ISR are the biggest levers.
Cache-Control with s-maxage and stale-while-revalidate reduces origin load dramatically.
Bundle analysis reveals oversized dependencies — run ANALYZE=true next build to visualise.
🗂 Rendering Strategies and Router Comparison
When to use each rendering strategy and router system
FeaturePages Router (pages/)App Router (app/)
Introduced in versionNext.js 1 (original)Next.js 13
Default component typeClient ComponentServer Component
Data fetching methodgetStaticProps / getServerSidePropsasync/await directly in component
ISR supportrevalidate in getStaticProps{ next: { revalidate: N } } in fetch
Client interactivityAll components can use hooksRequires 'use client' directive
LayoutsCustom _app.js + _document.jsNested layout.js files per folder
Learning curveLower — familiar React patternsHigher — new mental model required
Production readinessBattle-tested, widely usedStable since Next.js 13.4
Best forExisting projects, teams new to Next.jsNew projects, performance-critical apps

🎯 Key Takeaways

  • File-based routing means your folder structure in pages/ or app/ IS your navigation — no router config to maintain
  • SSG (getStaticProps) builds HTML at deploy time and serves from CDN — always default to this
  • ISR (adding revalidate: N to getStaticProps) gives you the speed of static with near-real-time freshness
  • getServerSideProps runs a server function on every request — use it only when data must be fresh per-request
  • In the App Router, components are Server Components by default — only add 'use client' when you need interactivity
  • Cache-Control headers and next/image optimisation are the biggest production performance levers

⚠ Common Mistakes to Avoid

    Using getServerSideProps everywhere for 'freshness'
    Symptom

    Pages feel slow, Time to First Byte is high, server costs are unexpectedly large. Under traffic spikes, the server cannot generate enough HTML pages per second and starts dropping connections.

    Fix

    Default to getStaticProps with revalidate (ISR). Only reach for getServerSideProps when you genuinely need per-request data like session cookies or live stock prices. SSR runs a server function on every single page load — it is expensive and often unnecessary.

    Putting a plain <a> tag inside next/link
    Symptom

    In older Next.js versions, you get a warning 'Do not use <a> inside <Link>'. In Next.js 13+, if you accidentally wrap a Link around an existing <a>, you get nested anchor tags which breaks accessibility and navigation.

    Fix

    In Next.js 13+, Link renders its own <a> tag automatically. Just put text or other non-anchor elements as children of Link. Only add an <a> tag as a child if you are on Next.js 12 or older.

    Ignoring the fallback option in getStaticPaths
    Symptom

    You deploy, a user visits a new product page that was not in your build, and they hit a 404 even though the product exists in your database.

    Fix

    Set fallback: 'blocking' in getStaticPaths. This tells Next.js to SSR any page not pre-built, then cache it for future visitors. Your build only pre-generates popular pages (keeping build times fast) while still serving any valid URL correctly.

    Not setting Cache-Control headers for static assets
    Symptom

    Every page load re-downloads JavaScript and CSS bundles from the origin server. Lighthouse reports high TTFB and recommends caching static assets. CDN cache hit rate is near zero.

    Fix

    Add Cache-Control headers in next.config.js: public, max-age=31536000, immutable for /_next/static/* assets. These files have content hashes in their filenames — they never change, so aggressive caching is safe.

    Using next/image without width and height props
    Symptom

    Images cause Cumulative Layout Shift (CLS) — content jumps around as images load. Lighthouse CLS score is above 0.25 (poor). Google search ranking is penalised.

    Fix

    Always provide width and height to next/image. These props set the aspect ratio so the browser reserves space before the image loads. For responsive images, use fill={true} with a sized parent container instead.

Interview Questions on This Topic

  • QWhat is the difference between getStaticProps and getServerSideProps, and how do you decide which one to use for a given page?Mid-levelReveal
    getStaticProps runs at build time — the HTML is generated once and served from a CDN. It is the fastest rendering strategy because no server work happens per request. Use it for content that does not change often: blog posts, product pages, marketing pages, documentation. getServerSideProps runs on every request — the server fetches data and builds HTML before sending it to the user. It is the most expensive strategy. Use it only when data must be fresh per-request AND needs to be in the initial HTML: dashboards showing live metrics, pages dependent on session cookies, personalised feeds. The decision framework: if the data can be stale for N seconds, use getStaticProps with revalidate: N (ISR). If the data must be fresh and the page must be SEO-indexed, use getServerSideProps. If the data is behind authentication and does not need SEO, use client-side fetching (useEffect or React Query) and skip server rendering entirely.
  • QWhat are React Server Components in Next.js's App Router, and why can't you use useState or useEffect inside them?Mid-levelReveal
    React Server Components (RSCs) are components that execute only on the server. They render to an RSC payload — a special format that the client can reconstruct into React elements — with zero JavaScript shipped to the browser for those components. You cannot use useState or useEffect in Server Components because these hooks depend on the React client runtime. useState manages client-side state that persists across re-renders in the browser. useEffect runs side effects after the component mounts in the browser. Server Components do not mount in the browser — they render once on the server and send their output as HTML or RSC payload. There is no client runtime to execute these hooks. If a component needs interactivity (state, effects, event handlers), add 'use client' at the top of the file. This tells Next.js to include the component in the client JavaScript bundle. The key principle: keep as many components as Server Components as possible — only make components client-side when they specifically need browser APIs or React hooks.
  • QIf a page uses getStaticProps with revalidate: 60, and two users hit that page within the same minute — what does each user actually receive? Walk me through what Next.js does behind the scenes.SeniorReveal
    This is ISR (Incremental Static Regeneration) in action. First user (after the cache expires): Next.js serves the stale (cached) HTML immediately — the user does not wait. In the background, Next.js re-runs getStaticProps, generates fresh HTML, and replaces the cached version. The first user sees the old data. Second user (within the 60-second window): Next.js serves the freshly generated HTML from the background revalidation. This user sees the new data. If no user visits for 60+ seconds: the cache is stale but not yet regenerated. The next user triggers the background regeneration and sees stale data. The user after that sees fresh data. The key insight: ISR never makes a user wait for revalidation. The stale-while-revalidate pattern means every user gets an instant response — either from the existing cache or from a background-generated replacement. No user is ever blocked waiting for getStaticProps to complete. This is why ISR is superior to SSR for most use cases: the user experience is identical to static (instant), but the data freshness approaches real-time (within the revalidation window).
  • QWhat is the difference between the Pages Router and the App Router in Next.js?JuniorReveal
    The Pages Router is the original routing system — files in the pages/ folder become routes. Components are Client Components by default. Data fetching uses getStaticProps (SSG) and getServerSideProps (SSR). It has been battle-tested since Next.js 1. The App Router is the newer system introduced in Next.js 13 — files in the app/ folder become routes. Components are Server Components by default (zero JS shipped). Data fetching uses async/await directly in components with fetch() caching options. It supports nested layouts, streaming, and React Server Components. The key differences: App Router ships less JavaScript (Server Components), has a simpler data fetching model (just use fetch), and supports nested layouts per route segment. Pages Router is simpler to learn and is used in most existing codebases. New projects should use the App Router. Existing projects can migrate gradually — both routers coexist in the same project.

Frequently Asked Questions

Do I need to know React before learning Next.js?

Yes — Next.js is a framework built on top of React, not a replacement for it. You should be comfortable with React components, props, state, and hooks before starting with Next.js. The framework handles routing, rendering strategy, and optimisation on top of the React you already know.

Is Next.js only for server-side rendering?

No — this is one of the biggest misconceptions. Next.js supports static generation (SSG), server-side rendering (SSR), client-side rendering (CSR), and incremental static regeneration (ISR) all in the same project, on a page-by-page basis. Most production apps use a mix of all four strategies depending on what each page needs.

What is the difference between the app/ folder and the pages/ folder in Next.js 13+?

They represent two different routing and rendering systems that can coexist in one project. The pages/ folder uses the original Pages Router with getStaticProps and getServerSideProps. The app/ folder uses the newer App Router where components are Server Components by default and data fetching happens with async/await directly in components. New projects should use the App Router; existing projects can migrate gradually.

Can I deploy Next.js without Vercel?

Yes. Next.js deploys anywhere: AWS (with SST or Amplify), Google Cloud, Docker containers, Netlify, or static hosting for fully-static sites. Vercel provides the tightest integration (automatic ISR caching, edge functions, preview deployments) but is not required. For self-hosted deployments, use next start with a Node.js server or export a static site with next export for fully-static applications.

How do I know which rendering strategy a page is using?

Run next build — the output shows each page with a symbol: ● (static/SSG), λ (server/SSR), or ○ (static with ISR). In development mode, check the terminal output or use the Next.js DevTools browser extension. You can also check the response headers — static pages have different cache headers than server-rendered pages.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousReact Testing with JestNext →React Custom Hooks
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged