{}The Interview
Handbook

Tracks / Next.js

Next.js App Router, rendering & caching

mid 10 questions · 6 min read nextjsrsccachingssr

Questions in this set 10
  1. 01Compare CSR, SSR, SSG and ISR. When do you pick each?
  2. 02Server Components vs Client Components — what actually goes in each?
  3. 03Explain Next.js caching. Why is my data stale?
  4. 04What are Server Actions and when would you use one instead of a route handler?
  5. 05What do loading.tsx, error.tsx, layout.tsx and template.tsx do?
  6. 06How do you fetch data in parallel and avoid request waterfalls?
  7. 07How does routing work — dynamic segments, route groups, and parallel routes?
  8. 08What is middleware good for, and what should never go in it?
  9. 09How do you optimise a Next.js app for Core Web Vitals?
  10. 10How do you handle authentication in the App Router?
01

Compare CSR, SSR, SSG and ISR. When do you pick each?

strategy HTML built best for trade-off
CSR in the browser dashboards behind auth poor SEO, slow first paint
SSR per request personalised, always-fresh pages server cost, TTFB depends on your DB
SSG at build marketing, docs, blogs rebuild to update; slow builds at scale
ISR at build + revalidated in background large catalogues that change occasionally eventual consistency; stale window

In the App Router these are not page-level modes any more but consequences of what you do: a route is static unless it reads request-time data (cookies(), headers(), searchParams, an uncached fetch), and export const revalidate = 60 gives you ISR. export const dynamic = "force-dynamic" and force-static override the inference.

02

Server Components vs Client Components — what actually goes in each?

Everything is a Server Component by default. "use client" marks a boundary: that file and everything it imports ship to the browser.

Use a Client Component when you need state, effects, event handlers, browser APIs, or a library that uses any of those. Otherwise stay on the server — you keep the JS out of the bundle and get direct data access.

tsx
// app/products/[id]/page.tsx  — Server Component
export default async function Page({ params }) {
  const { id } = await params;                 // Next 15: params/searchParams are async
  const product = await db.product.find(id);   // no API layer, no useEffect, no loading state
  return <><Details product={product} /><AddToCart id={product.id} /></>;
}

// components/AddToCart.tsx
"use client";
export function AddToCart({ id }) { const [n, setN] = useState(1); … }

The pattern to name: push client boundaries to the leaves. Do not put "use client" at the top of a page just because one button needs an onClick.

03

Explain Next.js caching. Why is my data stale?

There are four layers, and "stale data" is almost always one of them:

  1. Request memoisation — identical fetches within one render pass are deduped. Per-request, automatic.
  2. Data Cache — persistent, across requests and deploys. Controlled by fetch(url, { next: { revalidate: 60, tags: ["products"] }, cache: "no-store" }).
  3. Full Route Cache — the rendered HTML/RSC payload of static routes, on the server.
  4. Router Cache — the client-side cache of RSC payloads for visited routes, so back/forward is instant.

Invalidation: revalidatePath("/products") and revalidateTag("products") from a Server Action or route handler — call them right after a write. Time-based revalidate for content that can be a bit stale.

Note the version difference: in Next 14 fetch was cached by default; in Next 15 fetch and route handlers are uncached by default and you opt in. Getting this right, and saying which version you mean, is a strong signal.

04

What are Server Actions and when would you use one instead of a route handler?

A Server Action is a function marked "use server" that the client can invoke over an RPC-style POST — no API route, no fetch wrapper, and it works without JavaScript when used as a form action.

tsx
// app/actions.ts
"use server";
import { revalidateTag } from "next/cache";
import { z } from "zod";

export async function createTodo(prev, formData) {
  const session = await auth();                       // ALWAYS authenticate inside the action
  if (!session) return { error: "unauthorized" };     // it is a public HTTP endpoint
  const parsed = z.object({ title: z.string().min(1).max(200) }).safeParse({
    title: formData.get("title"),
  });
  if (!parsed.success) return { error: "Title is required" };
  await db.todo.create({ ...parsed.data, userId: session.userId });
  revalidateTag("todos");
  return { ok: true };
}

Use an Action for mutations from your own UI. Use a Route Handler (app/api/**/route.ts) when you need a real public API: third-party callers, webhooks, non-POST verbs, custom headers, streaming, or a stable contract for mobile clients.

The security point interviewers want: a Server Action is a public endpoint. Anyone can call it with arbitrary arguments. Authenticate, authorise and validate inside every one.

05

What do loading.tsx, error.tsx, layout.tsx and template.tsx do?

  • layout.tsx — shared shell; does not re-render on navigation between its children, so it preserves state.
  • template.tsx — same position, but a fresh instance per navigation (for enter animations or per-route effects).
  • loading.tsx — automatic <Suspense> boundary for the segment; the shell streams immediately while the page awaits data.
  • error.tsx — a client-side error boundary for the segment with a reset() function. global-error.tsx catches errors in the root layout.
  • not-found.tsx — rendered by notFound().

Streaming is the reason this matters: with loading.tsx plus nested <Suspense> you send the layout and header immediately and let slow widgets fill in, instead of blocking TTFB on the slowest query.

06

How do you fetch data in parallel and avoid request waterfalls?

Sequential awaits in one component serialise your latency. Kick off promises first, or use Promise.all:

tsx
export default async function Page() {
  const userP = getUser();          // start both
  const postsP = getPosts();
  const [user, posts] = await Promise.all([userP, postsP]);
  …
}

For independent slow sections, do not await at all — render a Suspense boundary per section so each streams in as it resolves. Also preload patterns: call the data function (without awaiting) in the layout to warm the request cache before the child needs it.

07

How does routing work — dynamic segments, route groups, and parallel routes?

text
app/
  (marketing)/page.tsx          # route group: organisation only, not in the URL
  blog/[slug]/page.tsx          # dynamic segment
  shop/[...categories]/page.tsx # catch-all
  docs/[[...slug]]/page.tsx     # optional catch-all
  dashboard/@team/page.tsx      # parallel route slot, rendered as a `team` prop of the layout
  photos/(.)[id]/page.tsx       # intercepting route: modal over the current page

generateStaticParams pre-renders dynamic routes at build time; generateMetadata produces per-route SEO metadata (and can await data). Intercepting + parallel routes together are how the "click a photo, get a modal, but a hard refresh shows the full page" pattern is built — a nice thing to be able to describe.

08

What is middleware good for, and what should never go in it?

middleware.ts runs on the Edge runtime before a request is matched to a route: redirects, rewrites, locale detection, A/B bucketing, and a cheap auth check (is a session cookie present?). It must be fast — it is on the path of every matched request — and it runs in a limited runtime with no Node APIs and no database drivers.

What should not go there: full session validation against your database, business logic, or anything slow. Do the real authorisation check in the page/layout/action, close to the data. Middleware that merely checks cookie presence is a UX optimisation, not a security boundary.

09

How do you optimise a Next.js app for Core Web Vitals?

  • next/image — automatic resizing, modern formats, lazy loading; set priority on the LCP image and always give sizes for responsive images.
  • next/font — self-hosts and preloads fonts with font-display: swap, eliminating a render-blocking request and the layout shift from a late font swap.
  • next/script with strategy="afterInteractive" or "lazyOnload" for third-party tags — usually the largest single win.
  • next/dynamic for heavy client-only widgets (charts, editors, maps).
  • Move work to Server Components so it never reaches the bundle; run @next/bundle-analyzer to see what does.
  • Static or ISR wherever the page is not personalised, so the CDN serves it.
10

How do you handle authentication in the App Router?

Sessions in an HttpOnly; Secure; SameSite=Lax cookie. Read and verify it in a small auth() helper; call that helper in every Server Component, Server Action and Route Handler that touches protected data — not once in middleware. Use React.cache around it so the verification runs once per request. For the client, pass only the safe fields (id, name, roles) down as props; never send the token itself into a Client Component, because anything crossing that boundary is serialised into the HTML.