Lesson 11 of 2224 min read

Server-Side Rendering (SSR) in Next.js Explained with Examples

Learn how Server-Side Rendering works in the Next.js App Router, what triggers dynamic rendering, and how per-request data fetching works.

Author: CodersNexus

Server-Side Rendering (SSR) in Next.js Explained with Examples

Static Site Generation is fast, but it has one hard constraint: the content is the same for everyone, fixed at build time. Many real pages can't work that way — a page showing a logged-in user's account balance, a search results page reflecting live query parameters, or a dashboard displaying data that must never be more than a few seconds old. These pages need Server-Side Rendering: generating fresh HTML on the server for every single request.

This lesson explains exactly how SSR works in the Next.js App Router — including the fact that, unlike the older Pages Router, you don't 'choose' SSR by calling a special function. Instead, Next.js automatically renders a page dynamically (per request) whenever it detects that page relies on something that can only be known at request time, such as cookies, headers, or search parameters. We'll cover what triggers this dynamic behavior and how to fetch fresh, per-request data correctly.

Learning Objectives

  • Explain how Server-Side Rendering works in the App Router.
  • Identify what causes a page to render dynamically instead of statically.
  • Fetch per-request data safely inside a Server Component.
  • Use cookies and headers to build request-aware pages.
  • Understand the performance and freshness tradeoffs of SSR compared to SSG.

Core Definitions

  • Server-Side Rendering (SSR): A rendering strategy where a page's HTML is generated fresh on the server for every single incoming request.
  • Dynamic rendering: Next.js's term for rendering a route on-demand, per request, rather than pre-rendering it statically at build time.
  • Dynamic API: A function or data source, like cookies(), headers(), or searchParams, whose value can only be known at the time of an actual incoming request.
  • force-dynamic: A route segment config option that explicitly forces a page to always render dynamically, even if it could otherwise be statically rendered.
  • Per-request data fetching: Fetching fresh data specifically for the current incoming request, rather than reusing cached data from a previous build or request.

Detailed Explanation

Unlike the Pages Router, where you'd explicitly export a function called getServerSideProps to opt into SSR, the App Router takes a more automatic approach: Next.js analyzes each route and determines whether it can be statically rendered or must be dynamically rendered per request, based on what that route actually does.

A route becomes dynamic automatically when it uses what are called dynamic APIs — functions whose result can only be known at the moment a real request arrives. The most common examples are reading cookies() (to check a user's session), reading headers() (to inspect request metadata like a user-agent or authorization token), or accessing searchParams (the query string parameters on the current URL, like ?query=shoes). Since none of these values exist yet at build time — they only exist once an actual visitor makes a request — any page using them cannot be pre-rendered statically; Next.js automatically falls back to rendering it fresh, on the server, for every request.

This automatic detection is powerful because you rarely need to think explicitly about 'choosing' SSR — you simply write a Server Component that reads a dynamic API, and Next.js handles the rest, rendering that route dynamically without any special configuration. For example, a search results page reading searchParams.query to filter results based on the current URL's query string is automatically rendered per request, since different visitors will have different search terms.

In cases where you want to explicitly force a route to always render dynamically — even if it doesn't currently use a dynamic API, perhaps because you know it will soon or you want to guarantee fresh data regardless of caching — you can export `export const dynamic = 'force-dynamic'` from that route's file, overriding Next.js's automatic detection.

The tradeoff of SSR is straightforward: every request triggers real server computation — fetching data, running your component's rendering logic, and generating HTML — which is inherently more expensive at scale than serving a pre-built static file. But the benefit is equally clear: the content is guaranteed fresh, accurate, and can be fully personalized to the specific request, something SSG fundamentally cannot offer on its own.

Diagram Description

Visualize the automatic static-vs-dynamic decision:

[Next.js analyzes a route]
├── Uses cookies(), headers(), or searchParams? --YES--> [Dynamic Rendering: fresh HTML per request]
├── Fetches data with { cache: 'no-store' }? --YES--> [Dynamic Rendering: fresh HTML per request]
├── Has 'export const dynamic = force-dynamic'? --YES--> [Dynamic Rendering: fresh HTML per request]
└── None of the above? --> [Static Rendering: HTML built once at build time]

Request flow for a dynamic route:
[User visits /search?q=shoes] --> [Server runs SearchPage fresh] --> [Reads searchParams.q = 'shoes'] --> [Fetches matching results] --> [Sends complete HTML for THIS specific request]

Comparison Table

TriggerEffect on RenderingExample
Reading cookies()Forces dynamic (SSR) renderingChecking a logged-in user's session cookie
Reading headers()Forces dynamic (SSR) renderingInspecting a request's Authorization header
Reading searchParamsForces dynamic (SSR) renderingFiltering results based on ?category=shoes
fetch with cache: 'no-store'Forces dynamic (SSR) rendering for that fetchAlways fetching the latest live stock price
export const dynamic = 'force-dynamic'Explicitly forces dynamic renderingGuaranteeing freshness regardless of other factors

Next.js Practical Example

// app/dashboard/page.tsx — automatically dynamic (reads cookies)
import { cookies } from 'next/headers';

async function getUserData(sessionToken: string) {
  const res = await fetch('https://api.example.com/me', {
    headers: { Authorization: `Bearer ${sessionToken}` },
    cache: 'no-store', // always fetch fresh data for this request
  });
  return res.json();
}

export default async function DashboardPage() {
  const cookieStore = await cookies();
  const sessionToken = cookieStore.get('session')?.value ?? '';
  const user = await getUserData(sessionToken);

  return <h1>Welcome back, {user.name}</h1>;
}

// app/search/page.tsx — automatically dynamic (reads searchParams)
export default async function SearchPage({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>;
}) {
  const { q } = await searchParams;
  const results = q ? await searchProducts(q) : [];

  return <p>Found {results.length} results for "{q}"</p>;
}

DashboardPage reads cookies() to retrieve the current visitor's session token — a value that only exists once a real request with real cookies arrives, so Next.js automatically renders this entire page dynamically, per request, without any manual configuration. The fetch call additionally specifies cache: 'no-store', explicitly ensuring the user's data is always fetched fresh rather than reused from any cache. SearchPage similarly becomes dynamic automatically because it reads searchParams — the query string on the incoming URL — which is different for every visitor's search and therefore cannot be known or pre-rendered at build time.

Industry Examples

  • Banking and fintech dashboards use SSR to render account balances and recent transactions fresh on every request, ensuring users never see stale financial data.
  • E-commerce search and filter pages use SSR (triggered automatically by reading searchParams) so results always reflect the exact query and filters a shopper selected.
  • SaaS admin panels render user-specific settings and permissions via SSR, reading session cookies to determine what each individual user is allowed to see.
  • News sites use SSR for live, frequently updated sections like a homepage's 'breaking news' feed, ensuring visitors always see the latest headlines rather than a stale build.
  • Airlines and travel booking sites use SSR for search results pages, since flight availability and pricing must reflect real-time inventory rather than a pre-built static snapshot.

Interview Questions and Answers

Q1. How does the Next.js App Router decide whether a page is statically or dynamically rendered?

Next.js automatically analyzes each route: if it uses a dynamic API like cookies(), headers(), or searchParams, or fetches data with cache: 'no-store', or explicitly sets `export const dynamic = 'force-dynamic'`, the route is rendered dynamically (per request). Otherwise, it defaults to static rendering at build time.

Q2. What is a 'dynamic API' in the context of Next.js rendering?

A dynamic API is a function or data source whose value can only be known when an actual request arrives — such as cookies(), headers(), or the current URL's searchParams — since these values don't exist yet at build time and differ per visitor or request.

Q3. How do you explicitly force a route to always render dynamically?

By exporting `export const dynamic = 'force-dynamic'` from that route's file, which overrides Next.js's automatic static/dynamic detection and guarantees the route always renders fresh, per request.

Q4. What is the main tradeoff of SSR compared to SSG?

SSR guarantees fresh, accurate, request-specific content but requires real server computation on every single request, which is more resource-intensive at scale than SSG's approach of serving pre-built static files with no per-request computation.

Q5. Why does reading cookies() in a Server Component cause that route to become dynamic?

Because cookies are sent as part of an actual HTTP request and can differ for every single visitor (e.g., different session tokens), their value cannot be known ahead of time at build. Next.js therefore must render that page fresh, per request, whenever cookies() is used.

MCQs With Answers

1. Which of the following automatically makes a Next.js App Router page render dynamically?

  1. Fetching data with default caching
  2. Reading cookies() inside the component
  3. Rendering static text only
  4. Using a CSS module

Answer: B. Reading cookies() inside the component

Explanation: Reading cookies() is a dynamic API whose value only exists at request time, forcing Next.js to render that route dynamically per request.

2. How do you explicitly force a route to always render dynamically in the App Router?

  1. export const static = false
  2. export const dynamic = 'force-dynamic'
  3. Add a 'use dynamic' directive
  4. Set revalidate to 0 in next.config.js

Answer: B. export const dynamic = 'force-dynamic'

Explanation: This route segment config explicitly overrides automatic detection, forcing the route to always render dynamically per request.

3. What is the main cost tradeoff of SSR compared to SSG?

  1. SSR cannot handle dynamic routes
  2. SSR requires real server computation on every request
  3. SSR cannot fetch data at all
  4. SSR always produces larger HTML files

Answer: B. SSR requires real server computation on every request

Explanation: Unlike SSG's pre-built static files, SSR re-renders HTML fresh for every incoming request, consuming server resources proportional to traffic.

4. Why does reading searchParams typically make a page render dynamically?

  1. searchParams is always empty
  2. Its value differs per request and isn't known at build time
  3. It requires a special license
  4. It only works with SSG

Answer: B. Its value differs per request and isn't known at build time

Explanation: Since the query string can differ for every visitor's request, Next.js cannot pre-render a page relying on it at build time, so it renders dynamically instead.

5. In the Next.js App Router, how do you typically opt into SSR for a page?

  1. By calling getServerSideProps
  2. By using dynamic APIs like cookies(), headers(), or searchParams, which trigger it automatically
  3. By adding a .ssr file extension
  4. SSR must be manually enabled in next.config.js for every route

Answer: B. By using dynamic APIs like cookies(), headers(), or searchParams, which trigger it automatically

Explanation: Unlike the older Pages Router's explicit getServerSideProps function, the App Router automatically detects the need for SSR based on dynamic API usage within a route.

Common Mistakes to Avoid

  • Expecting to need a special function like getServerSideProps in the App Router, when dynamic rendering is triggered automatically by dynamic API usage.
  • Using cookies() or headers() in a page that doesn't actually need per-request data, unnecessarily forcing dynamic rendering and losing SSG's performance benefits.
  • Forgetting that fetch calls with { cache: 'no-store' } also trigger dynamic rendering for that route, even without reading cookies or headers.
  • Assuming SSR pages are automatically cached the same way static pages are, when in fact they compute fresh output on every single request by default.
  • Overusing force-dynamic out of caution, when a page's actual data requirements might allow for cheaper static or ISR-based rendering instead.

Interview Notes

  • SSR (dynamic rendering) generates a page's HTML fresh on the server for every incoming request.
  • Reading cookies(), headers(), or searchParams automatically triggers dynamic rendering in the App Router.
  • fetch calls with { cache: 'no-store' } also force dynamic rendering for that route.
  • export const dynamic = 'force-dynamic' explicitly forces dynamic rendering, overriding automatic detection.
  • SSR trades higher per-request server cost for guaranteed content freshness and personalization.

Key Takeaways

  • The App Router doesn't require you to explicitly 'choose' SSR — it's automatically triggered by using dynamic, request-time-only data sources.
  • cookies(), headers(), and searchParams are the three most common triggers for dynamic rendering.
  • SSR is the right tool specifically when content must be fresh, accurate, or personalized on every single request.
  • Understanding what triggers dynamic rendering helps you avoid accidentally losing SSG's performance benefits on pages that don't actually need per-request freshness.

Summary

Server-Side Rendering in the Next.js App Router generates a page's HTML fresh on the server for every incoming request, rather than once at build time. Unlike the older Pages Router's explicit getServerSideProps function, the App Router automatically detects when a route needs dynamic rendering — triggered by reading dynamic APIs like cookies(), headers(), or searchParams, or by fetching data with { cache: 'no-store' }. You can also explicitly force dynamic rendering using `export const dynamic = 'force-dynamic'`. SSR trades the raw speed and low cost of SSG for guaranteed content freshness, making it the right choice for personalized dashboards, live search results, and any page whose content genuinely must reflect the current moment rather than a snapshot from the last build.

Frequently Asked Questions

Server-Side Rendering (SSR) means generating a page's complete HTML on the server fresh for every single incoming request, rather than reusing a pre-built version from a previous build, ensuring the content is always current.

No. Unlike the older Pages Router's getServerSideProps, the App Router automatically detects when a route needs SSR based on whether it uses dynamic APIs like cookies(), headers(), or searchParams.

Dynamic APIs are functions like cookies(), headers(), and searchParams whose values can only be known once an actual request arrives — they don't exist at build time — so any page using them must be rendered fresh, per request.

Export `export const dynamic = 'force-dynamic'` from that route's file, which explicitly overrides Next.js's automatic static/dynamic detection.

For each individual request, yes — SSR requires real server computation every time, while SSG serves a pre-built file with no per-request work. However, SSR guarantees fresh content, a tradeoff that's worth it for personalized or rapidly changing data.

Yes, absolutely. Rendering strategy is determined per route, so you can have some pages statically generated (like a blog) and others server-rendered per request (like a dashboard) within a single application.

Not necessarily — default fetch caching behavior generally supports static rendering. It's specifically using { cache: 'no-store' }, or dynamic APIs like cookies()/headers()/searchParams, that triggers dynamic rendering.

Because session cookies are unique per visitor and only exist once an actual request arrives with them attached, Next.js cannot know their value at build time, so it must render that page fresh for each request to correctly reflect the specific user's session.

No, both deliver complete, fully-rendered HTML to search engine crawlers. The difference is purely about when that HTML is generated (build time for SSG, request time for SSR), not whether the content is crawlable.

Using a dynamic API like cookies() will force that route to render dynamically regardless of generateStaticParams, since dynamic APIs fundamentally require per-request rendering — the two approaches are generally mutually exclusive for the same route.