Next.js Interview Questions · 2026

Next.js Interview Questions (2026): Most Asked, Answered

A candidate at a Series B startup got through the App Router file conventions fine, layouts, loading states, route groups, and then stalled on one question: why did the same fetch call get cached on one page and re-run on every request on another, with no cache option set on either. He'd shipped Next.js for two years and had never once had to reason about it, because the framework had quietly picked a default for him every time. Next.js currently ships as version 16.2.10, and the App Router's rendering model, Server Components by default, opt-in Client Components, several distinct caching layers stacked on top of each other, is exactly the part of the framework most candidates have used without fully understanding (Next.js documentation, Server and Client Components).

Here's the pattern across most interview loops: the first few questions are routing and rendering vocabulary, SSR versus SSG versus ISR, App Router versus Pages Router, and those questions filter out people who've never opened the docs. The questions that actually separate a senior candidate show up one layer down, in the caching model, in the exact mechanics of when a Server Component re-fetches versus when it doesn't, and in newer changes candidates who learned Next.js a year or two ago haven't caught up on yet. One of those changes is real and recent: as of Next.js 16, the file you'd call middleware.ts is being renamed to proxy.ts, and the older name is deprecated (Next.js documentation, proxy.js). Plenty of prep material and even plenty of production codebases haven't caught up.

This page covers Next.js interview questions across nine areas: core rendering concepts, App Router file conventions, the Server/Client Component split, data fetching and the caching layers, the legacy Pages Router methods that still show up in interviews on older codebases, API routes and proxy (formerly middleware), image and font optimization, deployment and runtimes, and a set of edge cases that tend to catch people who've only worked from tutorials.

~50Questions
16.2.10Current Version
React Server ComponentsRendering Model
Proxy (v16)Middleware Renamed To

Core rendering concepts: SSR, SSG, ISR, and the App Router

Every Next.js loop opens near here. A shaky answer on this section is a bad sign regardless of how strong the rest of the resume looks.

Easy questions

15

Next.js is a React framework that adds file-based routing, several rendering strategies (server rendering, static generation, streaming), built-in code splitting, an image and font optimization pipeline, and a way to write server-only code, Server Components and Server Actions, that never ships to the browser as JavaScript. Plain React on its own gives you components and state, nothing about how a page gets to the browser or where data fetching runs.

The part that actually matters in an interview answer is the last one. A plain React SPA renders everything client-side by default, shipping a bundle and then fetching data after the page loads. Next.js flips that default: a page starts as HTML rendered on the server, with only the interactive pieces hydrated afterward.

The Pages Router, everything under a top-level pages/ directory, is the original Next.js routing system. Every file exports a React component that's a full page, data fetching happens through exported functions like getServerSideProps or getStaticProps, and every component in that tree renders on both server and client by default.

The App Router, under app/, is the newer model built around React Server Components. Components are server-only by default unless marked with 'use client', routing is defined by folder structure with special files like layout.tsx, page.tsx, and loading.tsx, and data fetching happens by calling fetch or your database client directly inside an async component instead of through a special exported function. Both routers can coexist in the same project during a migration, but new projects default to the App Router.

SSR means the server generates the full HTML for a page on every request, using whatever data the page needs at that moment, and sends that HTML to the browser instead of an empty shell. The browser gets something visible immediately, then React hydrates the page to attach event handlers and make it interactive.

In the App Router, a page becomes SSR (Next.js calls this dynamic rendering) the moment it reads something request-specific, cookies, headers, search params, or fetches with caching disabled. In the Pages Router, exporting getServerSideProps forces the same behavior explicitly.

SSG means the HTML for a page is generated once, at build time, and reused for every visitor until the next build or revalidation. There's no per-request server work at all, the HTML is just a file served from a CDN edge.

Choose SSG for pages where the content doesn't depend on who's asking and doesn't change every minute, marketing pages, blog posts, documentation, product pages that update a handful of times a day. Choose SSR when the content genuinely differs per request, a logged-in dashboard, search results filtered by query params, anything reading a cookie to decide what to show.

ISR lets a statically generated page get regenerated in the background after it's already been deployed, without a full rebuild of the whole site. You set a revalidation window, and after that window passes, the next request triggers a fresh render behind the scenes while still serving the old cached HTML immediately, then swaps in the new version once it's ready.

typescript
// app/products/[id]/page.tsx
export const revalidate = 3600; // regenerate at most once per hour

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
 const { id } = await params;
 const product = await fetch(`https://api.example.com/products/${id}`).then((r) => r.json());
 return <h1>{product.name}</h1>;
}

ISR solves the real problem that pure SSG has at scale: a site with ten thousand product pages can't rebuild all of them every time one price changes. ISR lets each page refresh independently, on its own schedule, without touching the others.

Every route corresponds to a folder inside app/, and the folder needs a page.tsx file inside it to actually be reachable as a URL. A folder without a page.tsx is just a routing segment, useful for organizing layouts or nested routes, but it doesn't render anything on its own.

typescript
app/
 page.tsx     // "/"
 about/
  page.tsx    // "/about"
 blog/
  [slug]/
   page.tsx   // "/blog/:slug"

A loading.tsx file automatically wraps its sibling page.tsx in a React Suspense boundary and renders as the fallback while the page's data is being fetched on the server. You don't write the Suspense boundary yourself, Next.js wires it up from the file convention alone.

typescript
// app/dashboard/loading.tsx
export default function Loading() {
 return <p>Loading dashboard...</p>;
}

This is what makes streaming work without extra code: the shell of the layout renders and shows immediately, the loading state shows in place of the slow part, and the real content swaps in once the data resolves, all without a client-side spinner state you'd have to manage by hand.

Add 'use client' the moment a component needs state, an effect, an event handler, or a browser-only API, useState, useEffect, onClick, localStorage, window. Everything else, by default, is a Server Component.

typescript
'use client';

import { useState } from 'react';

export default function Counter() {
 const [count, setCount] = useState(0);
 return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

The directive marks a boundary, not just one file. Every component imported into that file and rendered inside it also becomes part of the client bundle, so the common mistake is marking a whole layout as a Client Component because one small button inside it needs state, instead of isolating just that button.

Plain fetch calls get request memoization automatically, but a database query or an ORM call doesn't. Wrapping that function in React's cache() gives it the same deduplication behavior within a single render pass, so calling it from multiple components doesn't run the same query multiple times.

typescript
import { cache } from 'react';

export const getUser = cache(async (id: string) => {
 return db.user.findUnique({ where: { id } });
});

getStaticProps runs at build time (or on-demand with ISR's revalidate option) in the Pages Router, fetching data once and baking the result into the pre-rendered HTML for that page. It never runs in the browser and never runs per-request unless a revalidation window triggers a regeneration.

typescript
export async function getStaticProps() {
 const posts = await fetch('https://api.example.com/posts').then((r) => r.json());
 return { props: { posts }, revalidate: 60 };
}

getServerSideProps runs on the server on every single request, before the page renders, and whatever it returns becomes that page's props for that one request only. Where getStaticProps trades freshness for speed by baking data in ahead of time, getServerSideProps trades speed for guaranteed-fresh, per-request data, useful for pages that genuinely need to reflect the current logged-in user or a live value.

A Pages Router API route is a file in pages/api/ that exports a single default function receiving Node-style req and res objects, one function handles every HTTP method unless you branch on req.method yourself. An App Router Route Handler is a route.ts file that exports one function per HTTP verb, GET, POST, PUT, DELETE, using the standard Web Request and Response objects instead of Node's.

typescript
// app/api/posts/route.ts
export async function GET() {
 const posts = await db.post.findMany();
 return Response.json(posts);
}

export async function POST(request: Request) {
 const body = await request.json();
 const post = await db.post.create({ data: body });
 return Response.json(post, { status: 201 });
}

next/image automatically serves images in modern formats like WebP or AVIF when the browser supports them, resizes the image to match the actual rendered size using srcset, lazy-loads offscreen images by default, and reserves the correct space for the image before it loads to prevent layout shift.

typescript
import Image from 'next/image';

<Image src="/hero.jpg" alt="Hero" width={1200} height={600} />

A plain img tag does none of that on its own, you'd have to hand-write srcset variants, pick a format, and manage loading behavior yourself, or bring in a separate library to do it.

next/font downloads the font file at build time and self-hosts it alongside the rest of the app's static assets, instead of the browser making a separate request to a third-party font host at runtime. That removes an external network round trip and the layout shift that comes from text rendering in a fallback font before the real one loads in.

typescript
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export default function Layout({ children }: { children: React.ReactNode }) {
 return <html className={inter.className}>{children}</html>;
}

In production, next/link automatically prefetches the linked route's RSC payload when the link scrolls into the viewport, so by the time a user actually clicks it, the data needed for that page is often already sitting in the Router Cache. That's what makes client-side navigation between prefetched routes feel instant compared to a fresh page load.

typescript
import Link from 'next/link';

<Link href="/about">About</Link>

This behavior is disabled in local development to keep the dev server responsive, which is why prefetching performance is one of those things you have to actually test against a production build to evaluate honestly.

Medium questions

25

A dynamic segment, [id], matches exactly one URL segment and gives you a single value, /blog/hello matches [id] as "hello", but /blog/hello/world doesn't match at all. A catch-all segment, [...slug], matches one or more segments and gives you an array, so /docs/a/b/c matches [...slug] as ["a", "b", "c"].

There's a third variant, [[...slug]], double brackets, that makes the catch-all optional, so it also matches the parent route with zero segments, /docs itself. That variant is the one people forget exists until they need a docs site where the index page and every nested page share one component.

A route group is a folder named with parentheses, (marketing), that organizes routes without adding a segment to the actual URL. It exists purely for structuring the codebase, applying a shared layout to a set of routes, or splitting a large app into logical sections, none of which shows up in the browser's address bar.

typescript
app/
 (marketing)/
  layout.tsx   // shared layout, applies to both routes below
  page.tsx    // "/"
  about/
   page.tsx   // "/about", not "/marketing/about"
 (shop)/
  layout.tsx   // a different shared layout
  cart/
   page.tsx   // "/cart"

A layout persists across navigations between its child routes and doesn't re-render or reset its state when the user moves from one child page to another. A template, by contrast, creates a fresh instance on every navigation, its state resets and its effects re-run, even though it looks visually identical to a layout in most cases.

Reach for a template instead of a layout when you specifically need per-navigation behavior, resetting a form on route change, re-triggering an entrance animation, or restarting a useEffect that tracks page views. For everything else, a layout is the right default because it avoids the unnecessary re-render cost.

Parallel routes let you render more than one page inside the same layout at the same URL, using named slots, folders prefixed with @, each independently rendered with its own loading and error states.

typescript
app/
 dashboard/
  @analytics/
   page.tsx
  @team/
   page.tsx
  layout.tsx   // receives { children, analytics, team } as props

The real use case is a dashboard where an analytics widget and a team activity feed need to load independently, one can show its own loading skeleton or error boundary without blocking or crashing the other. Modals are the other common use case, rendering a modal in one slot while the underlying page renders normally in another.

A Server Component renders entirely on the server and never ships its own code to the browser, only the HTML (and a compact RSC payload) it produces gets sent. It can be async, fetch data directly with await, read environment secrets, and query a database, none of which requires a special exported function.

A page using getServerSideProps in the Pages Router also runs on the server, but the component itself still ships to the client as regular JavaScript and hydrates normally. Server Components are a different model entirely: the component's own code, not just its data, stays server-side, which is why they cut client bundle size for anything that doesn't need interactivity.

The RSC payload is a compact, serialized representation of a rendered Server Component tree that the framework sends to the client. It contains the already-rendered output of the Server Components, placeholders marking where each Client Component should mount along with a reference to its JavaScript file, and any props a Server Component passed down to a Client Component (Next.js documentation, Server and Client Components).

On the initial load, the browser gets HTML for a fast visible page, then uses the RSC payload to reconcile which parts are actually Client Components and hydrate just those. On a later client-side navigation, the RSC payload gets fetched again for the new route and the Client Components on the page re-render entirely on the client, without a fresh HTML document.

A Server Component can import and render a Client Component directly, that's the normal composition pattern, a server-rendered page importing an interactive button. A Client Component cannot directly import a Server Component the same way, because once you're inside client-rendered code there's no server left to render that component on.

The workaround is passing the Server Component as children or another prop from a parent Server Component, so the Client Component just renders whatever was handed to it without needing to import or instantiate it itself. The Server Component is still fully rendered on the server ahead of time either way, it's just slotted in rather than imported.

Both depend on a persistent client-side runtime that survives across renders and re-renders in the browser. A Server Component runs once on the server to produce output and then it's done, there's no ongoing instance of it living in memory to hold state or subscribe to a context provider the way a mounted client component does.

typescript
'use client';

import { createContext } from 'react';

export const ThemeContext = createContext({});

export default function ThemeProvider({ children }: { children: React.ReactNode }) {
 return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}

The workaround is exactly what the code above shows: wrap the provider itself in a small Client Component and import it into a Server Component layout. The provider becomes the one client boundary, and everything nested inside it can still consume the context normally.

In a Server Component, you fetch data by calling fetch or a database client directly inside an async function component, using regular await, no special exported function required. Multiple components in the same tree can each fetch what they individually need, right next to where that data is used, instead of one top-level function gathering everything and passing it down as props.

typescript
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
 const { id } = await params;
 const post = await fetch(`https://api.example.com/posts/${id}`).then((r) => r.json());
 return <h1>{post.title}</h1>;
}

Setting cache: 'force-cache' on a fetch call tells Next.js to cache the response and reuse it across requests, treating that data as static until it's explicitly revalidated. Left unset, the default behavior depends on whether the fetch happens before or after a request-time API is read on that route, cached by default in a purely static route, not cached at all once cookies, headers, or search params are involved.

typescript
await fetch('https://api.example.com/posts', { cache: 'force-cache' });
await fetch('https://api.example.com/posts', { cache: 'no-store' }); // always fresh
await fetch('https://api.example.com/posts', { next: { revalidate: 60 } }); // ISR-style

A Server Action is an async function that runs only on the server but can be called directly from a Client Component, typically as a form's action handler, without you writing a separate API route and a fetch call to reach it. You mark it with the 'use server' directive, either at the top of the function or the top of the file.

typescript
'use server';

export async function createPost(formData: FormData) {
 const title = formData.get('title');
 await db.post.create({ data: { title } });
 revalidatePath('/posts');
}

Beneath that, it's still a POST request, Next.js generates an endpoint for it automatically, but you never write the fetch call or the route handler by hand. That's the actual value: mutations read like a normal function call from the component's perspective.

revalidatePath invalidates the cached data for a specific route, everything that route depends on gets marked stale and re-rendered on the next visit. revalidateTag invalidates every cached fetch call anywhere in the app that was tagged with that specific tag, regardless of which route or routes it appears on.

typescript
await fetch('https://api.example.com/posts', { next: { tags: ['posts'] } });

// later, in a Server Action:
revalidateTag('posts'); // invalidates that data everywhere it's used
revalidatePath('/blog'); // invalidates the /blog route specifically

Tag-based invalidation is the better default once the same piece of data shows up on more than one route, a product that appears both on its own page and in a shared "related products" widget elsewhere. Path-based invalidation is simpler and fine when the data is genuinely scoped to one route.

getStaticPaths tells Next.js which dynamic route values to pre-render at build time, for a page like [id].tsx, it returns the specific list of ids that should get their own static HTML file generated ahead of time.

typescript
export async function getStaticPaths() {
 const posts = await fetch('https://api.example.com/posts').then((r) => r.json());
 return {
  paths: posts.map((p: { id: string }) => ({ params: { id: p.id } })),
  fallback: 'blocking',
 };
}

The fallback option controls what happens for an id not included in that list. false means anything not pre-rendered 404s outright. true serves a loading state immediately and renders the real page in the background once the data resolves. 'blocking' waits on the server and only responds once the page is fully rendered, so the first visitor to a not-yet-generated path never sees a loading flash, just a slightly slower first response.

An exported revalidate number on the page or layout, combined with a fetch call (or an unstable_cache-wrapped function for non-fetch data sources), reproduces the same ISR behavior without the special function. The data fetching itself just moves inline into the async component.

typescript
// App Router equivalent of getStaticProps({ revalidate: 60 })
export const revalidate = 60;

export default async function Page() {
 const posts = await fetch('https://api.example.com/posts').then((r) => r.json());
 return <PostList posts={posts} />;
}

Query parameters come off the request's URL using the standard URL API, since a Route Handler receives a real Web Request rather than a pre-parsed object. The body gets read with await request.json() for JSON payloads, or await request.formData() for form submissions.

typescript
export async function GET(request: Request) {
 const { searchParams } = new URL(request.url);
 const query = searchParams.get('q');
 return Response.json({ query });
}

The matcher array restricts which request paths trigger the proxy function at all. Without a matcher, the function runs on every single request, including static assets under _next/static, image optimization requests, and files in public/, which is rarely what you actually want and can add real latency to requests that never needed the check in the first place.

typescript
export const config = {
 matcher: ['/((?!api|_next/static|_next/image|.*.png$).*)'],
};

A subtlety worth knowing: even a negative matcher pattern that appears to exclude _next/data requests still lets the proxy run against them intentionally, so a page you've protected can't accidentally leave its underlying data route unprotected by a matcher typo.

A/B testing and feature flag routing is a common one, rewriting a percentage of requests to a variant page based on a cookie or header, entirely before the actual page render starts. Geolocation-based rewrites are another, redirecting a request based on the country inferred from request headers to a locale-specific route without the client needing to know that logic exists.

typescript
export function proxy(request: NextRequest) {
 const bucket = request.cookies.get('ab-bucket')?.value ?? Math.random() < 0.5 ? 'a' : 'b';
 const response = NextResponse.next();
 response.cookies.set('ab-bucket', bucket);
 return response;
}

priority tells Next.js to preload the image and skip the default lazy-loading behavior, so the image starts fetching immediately instead of waiting until it's about to scroll into view. Set it on whatever image is the largest visible element above the fold, since that's usually what determines a page's Largest Contentful Paint score.

Setting it on every image on the page defeats the purpose, that just makes everything load eagerly and competes for bandwidth on the initial request, which can make the actually-important image slower to arrive, not faster.

beforeInteractive loads and executes a script before any page JavaScript runs, reserved for things the page genuinely can't function without, bot detection or a polyfill. afterInteractive, the default, loads the script early but after the page has become interactive, right for most analytics and tag manager scripts. lazyOnload defers loading until the browser is idle, appropriate for chat widgets or anything genuinely non-critical to the initial experience.

typescript
import Script from 'next/script';

<Script src="https://example.com/analytics.js" strategy="afterInteractive" />

The Node.js runtime gives a route full access to Node APIs, the file system, native npm packages, longer execution limits, and is the default for most routes. The Edge runtime runs on a lighter, V8-isolate-based environment distributed globally, with a much smaller API surface, no file system access, and stricter size and execution limits, in exchange for lower cold-start latency and execution closer to the requesting user.

Pick Edge for something latency-sensitive and simple, an auth check, a redirect, a geolocation lookup, nothing that needs a database driver with native bindings or a large dependency tree. Pick Node for anything doing real backend work, database queries, file processing, or using a library that isn't Edge-compatible.

Turbopack is Next.js's Rust-based bundler, built as a faster alternative to webpack for both local development and production builds, using incremental computation so it only re-processes the parts of the dependency graph that actually changed instead of re-bundling from scratch.

For an interview answer, the useful framing is that it's largely a drop-in replacement at the config level for most projects, most webpack-specific customizations and loaders don't carry over one-to-one, so a project with heavy custom webpack config is more likely to hit a rough edge migrating than one using mostly default settings.

A regular environment variable is only available on the server, in Server Components, Route Handlers, and Server Actions, and Next.js replaces any reference to it in client-bundled code with an empty string at build time, specifically to prevent secrets from leaking into the browser bundle by accident.

typescript
//.env.local
DATABASE_URL=postgres://...     // server only
NEXT_PUBLIC_ANALYTICS_ID=UA-12345  // inlined into the client bundle

A variable prefixed with NEXT_PUBLIC_ gets inlined directly into the client JavaScript bundle at build time, meaning it's visible to anyone who opens dev tools. Only use that prefix for values that are genuinely fine to expose publicly, an analytics ID, a public API base URL, never an API key or database credential.

A static export const metadata object works for pages where the title and description are fixed and known ahead of time. generateMetadata is an async function you export instead, used when the metadata depends on data the page also needs to fetch, a blog post's title coming from the same database query that renders the post content.

typescript
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
 const { id } = await params;
 const post = await fetch(`https://api.example.com/posts/${id}`).then((r) => r.json());
 return { title: post.title, description: post.excerpt };
}

Next.js deduplicates the fetch call if the same request is also made inside the page component itself, using the same request memoization that covers regular data fetching, so you're not paying for the same network call twice just because both functions need the same data.

An error.tsx file automatically wraps its sibling route in a React error boundary, catching any error thrown during rendering, including inside async Server Components, and replacing that part of the UI with a fallback instead of crashing the whole page. It has to be a Client Component itself, since error boundaries rely on React lifecycle behavior that doesn't exist in Server Components.

typescript
'use client';

export default function Error({ error, reset }: { error: Error; reset: () => void }) {
 return (
  <div>
   <p>Something went wrong: {error.message}</p>
   <button onClick={() => reset()}>Try again</button>
  </div>
 );
}

A try/catch inside the component only handles errors you've anticipated at that specific point in the code and lets you decide what to render inline. error.tsx is a safety net for anything that slips past that, an unhandled exception anywhere in the tree beneath it, and it comes with a built-in reset function to retry rendering the segment without a full page reload.

In recent Next.js versions, params and searchParams in the App Router are asynchronous, a Promise that has to be awaited before you can read anything off it, where the equivalent values in getServerSideProps were always plain synchronous objects. Code copied over from an older App Router example, or written by someone used to the synchronous Pages Router version, often destructures params.id directly without awaiting it first.

typescript
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
 const { id } = await params; // required, params is a Promise
 return <h1>{id}</h1>;
}

The compiler catches this in TypeScript once the types are set up correctly, but in plain JavaScript it fails silently in a confusing way, since accessing a property directly on an unresolved promise doesn't throw, it just gives you undefined. This is exactly the kind of small API surface change candidates who learned Next.js a year or two ago haven't necessarily re-checked since.

Hard questions

10

Intercepting routes let you load a different route's content within the current layout, so navigating to it feels like an overlay rather than a full page transition, while a hard refresh or direct link to that same URL still renders the full standalone page. The convention uses (.) for the same level, (..) for one level up, and (...) for the root, matched against the folder you're intercepting.

The pattern people recognize immediately is Instagram's photo modal: clicking a photo from the feed opens it as a modal over the feed, but sharing that same photo URL directly, or refreshing on it, loads it as its own full page. That's exactly the behavior intercepting routes are built to produce, and it's genuinely hard to replicate cleanly without this convention because you'd otherwise need separate client-side routing logic layered on top of the framework's router.

A hydration mismatch happens when the HTML React generates on the client during hydration doesn't match the HTML the server already sent, so React has to throw away part of the tree and re-render it from scratch on the client instead of reusing what's already in the DOM. The visible symptom is usually a console warning and, in bad cases, a flash of incorrect content.

The most common cause in real apps is anything non-deterministic between server and client: rendering new Date() or Math.random() directly in JSX, reading window or localStorage during the initial render instead of inside an effect, or a browser extension injecting attributes into the DOM before React hydrates. The fix pattern is almost always the same, defer the client-only value into a useEffect and render a stable placeholder on the first pass, or wrap the value in suppressHydrationWarning when the mismatch is truly cosmetic and expected, like a rendered timestamp.

Request Memoization deduplicates identical fetch calls made multiple times during a single render pass, on the server, so if three different components in the same tree all fetch the same URL, only one actual network request goes out. It resets on every new request and doesn't persist across requests.

The Data Cache sits underneath fetch itself and persists across requests and deployments, this is what cache: 'force-cache' and next.revalidate actually control. The Full Route Cache stores the rendered HTML and RSC payload for an entire static route, so a request can skip rendering altogether and serve the pre-built output. The Router Cache lives in the browser, caching RSC payloads for routes the user has already visited or Next.js has prefetched, so client-side back-and-forth navigation feels instant without re-fetching from the server. Each layer is invalidated differently, Request Memoization clears itself per request automatically, the Data Cache clears via revalidateTag/revalidatePath or a time window, the Full Route Cache clears when the underlying data cache entries it depends on are revalidated, and the Router Cache clears on a hard refresh or after a set duration.

dynamic = 'auto', the default, lets Next.js decide per-route based on what the code actually does, static if nothing request-specific is read, dynamic the moment cookies, headers, or an uncached fetch show up. dynamic = 'force-dynamic' overrides that inference entirely and forces the route to render fresh on every single request, equivalent to setting every fetch in that route to cache: 'no-store'.

The case where this actually matters in an interview answer: a route that looks static because it doesn't obviously read a cookie, but calls a third-party API wrapper that reads a cookie internally three files deep. 'auto' should still correctly detect that and mark the route dynamic, but candidates who've only skimmed the docs sometimes assume they need force-dynamic everywhere out of caution, which throws away caching they didn't need to give up.

Starting in Next.js 16, the file convention formerly called middleware.ts is renamed to proxy.ts, and the exported function is renamed from middleware to proxy, with the old name deprecated. The framework also switched Proxy's default runtime to Node.js in the same version. The Next.js team's stated reason is that "middleware" kept getting confused with Express-style middleware, encouraging people to put general application logic there instead of the narrow request-interception job it's actually meant for (Next.js documentation, proxy.js).

typescript
// proxy.ts (was middleware.ts before Next.js 16)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function proxy(request: NextRequest) {
 return NextResponse.redirect(new URL('/home', request.url));
}

export const config = { matcher: '/about/:path*' };

For an interview answer, the practical part is knowing both names exist right now and that a codemod, npx @next/codemod@canary middleware-to-proxy., handles the rename mechanically. A candidate who's only worked from a year-old tutorial will say "middleware" without blinking, which is fine, but not recognizing the newer name at all is a small signal that they haven't touched the framework's recent release notes.

A static export, output: 'export' in next.config.js, builds the entire app into plain HTML, CSS, and JS files with no Node.js server required at all, deployable to any static file host or CDN with zero server runtime. It's the option that makes Next.js usable on hosts that only serve static files.

The tradeoff is real: anything requiring a live server stops working, Route Handlers that do more than return static data, Server Actions, Image Optimization's on-demand resizing (it falls back to unoptimized images or a custom loader), middleware/proxy, and any route using cookies(), headers(), or dynamic rendering. Every route effectively has to behave like it was built with getStaticProps, fully knowable ahead of time with no per-request server logic.

A statically rendered route gets fully rendered during next build, HTML and the RSC payload get generated once and written to disk (or the Full Route Cache), and the deployed server just serves that pre-built output for every request until it's revalidated. A dynamically rendered route has no pre-built HTML to serve at all, the build only produces the compiled server code for that route, and every actual render happens per-request once the app is running.

The build log itself reflects this directly, Next.js prints each route with a symbol indicating whether it was pre-rendered as static content or marked as a server-rendered route on demand. Reading that output correctly is a fast way to confirm a page you intended to be static didn't accidentally become dynamic because of a stray cookie read somewhere in its component tree.

The most common reason is that Request Memoization only deduplicates fetch calls within a single request, it doesn't persist between separate requests. If the underlying fetch also isn't opted into the Data Cache, cache: 'force-cache' or a positive revalidate window, every new request re-runs that fetch fresh, which can return different results if the upstream data changed between requests even seconds apart.

The fix is almost always to be explicit rather than relying on the default. Either cache the fetch with an appropriate revalidate window if the data doesn't need to be instantly fresh, or accept that it's genuinely dynamic and stop being surprised when it varies. Assuming "it rendered on the server so it must be consistent" is the exact assumption that trips candidates on this question.

Because Request Memoization and the Data Cache are two entirely separate mechanisms solving two different problems. Memoization only prevents the same URL from being fetched twice inside one render, it resets completely once that request finishes. The Data Cache is what persists a response across separate requests, and it only applies when the fetch call explicitly opts in, cache: 'force-cache' or a next.revalidate value, rather than being on by default for every fetch everywhere.

So a fetch call with no caching options set, on a route that's already dynamic for some other reason, gets deduplicated once per request but genuinely re-fetched on the next one. That's expected behavior, not a bug, and recognizing that distinction is exactly what separates this question from the earlier easier ones about caching.

When a Client Component receives a Server Component as children, the Server Component was already fully rendered on the server before the Client Component ever runs, its output arrives as part of the RSC payload, not as a component the Client Component instantiates itself. The Client Component just has a slot to drop that already-rendered output into.

typescript
'use client';
export default function Modal({ children }: { children: React.ReactNode }) {
 return <div className="modal">{children}</div>;
}

// in a Server Component:
<Modal><Cart /></Modal> // Cart renders on the server, Modal only handles the wrapper

If you tried to import that same Server Component directly inside the Client Component's file instead, it would fail, because everything imported into a 'use client' file becomes part of the client module graph, and a Server Component can't run in the browser. Passing it as children sidesteps that entirely, since the Client Component never actually imports it, it just receives already-rendered output as a prop.

How to prepare for a Next.js interview in 2026

Skip another round of memorizing the routing file names. Build one small thing that actually forces you to reason about the caching model: a page that fetches the same data from two different components (to see request memoization work), sets an explicit revalidate window, and exposes a Server Action that calls revalidateTag to invalidate it on demand. Watching a stale response actually update after that call teaches the caching layers faster than reading the docs page about them a second time.

Read the release notes for the last two or three Next.js minor versions before your interview, not the whole changelog, just the headline items. The proxy rename covered above is exactly the kind of detail that's easy to miss if your last hands-on Next.js project was from a year ago, and interviewers who've kept up with it notice when a candidate's mental model is stuck on an older version without realizing it.

Get the reps in before the real thing

Explaining the difference between the Data Cache and the Router Cache on paper is not the same as defending that answer out loud when an interviewer follows up with "so what happens if I add a cookie read to that same route." LastRoundAI's mock interview mode runs live coding rounds with real-time follow-up questions in your browser, and the free plan includes 15 credits a month that reset monthly instead of piling up unused. Starter is $19/mo if a handful of sessions isn't enough runway.

Once your answers hold up under a follow-up, the slower part of the job hunt is usually just getting in front of enough frontend and full-stack roles that actually test Next.js instead of listing it as a nice-to-have. Auto-Apply queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.

Questions about either product go to contact@lastroundai.com. That's the only inbox we check.

How this list was built

Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.

What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.

If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.

Frequently asked questions

How long does it take to prepare for a Next.js interview?

If you already work with Next.js day to day, a focused week on the areas you avoid in practice is usually enough. Coming in cold, expect three to four weeks. The gap is rarely knowledge; it is being able to explain something you normally just use.

What Next.js topics come up most often?

Interviewers concentrate on the parts that cause production incidents rather than the parts that are pleasant to learn. Expect the fundamentals to be assumed and the follow-up questions to sit one layer below what a tutorial covers.

Do I need hands-on Next.js experience to pass?

It shows quickly either way. Textbook answers hold up until the interviewer asks what you did when it broke, and that is usually the question that separates candidates. A small real project you can discuss honestly beats a longer list of familiarity claims.

Is Next.js still worth learning in 2026?

For interview purposes the question is really whether the teams you are targeting use it, which is worth checking against their actual job postings rather than general popularity rankings. Where it is in use it tends to be deeply embedded and slow to replace.

Leave a Reply

Your email address will not be published. Required fields are marked *