Next.js App Router Interview Questions: 10 Questions with Clear Answers
The transition to the App Router represents the most significant architectural evolution in the React and Next.js ecosystem. In technical interviews for React and full-stack front-end roles, interviewers frequently probe your understanding of React Server Components (RSC), boundary rules, streaming, and modern data-fetching paradigms.
Interviewers want to see whether candidates understand where code actually runs—on the server at build time, on the server per request, on the Edge, or in the browser. Strong candidates articulate how Server Components reduce client bundle size, how hydration works across boundaries, and how caching and Server Actions streamline full-stack data flow.
This guide targets front-end developers with 0 to 5 years of experience preparing for junior to mid-level engineering interviews.
| # | Question | Level |
|---|---|---|
| 1 | What is the fundamental difference between Server Components and Client Components in Next.js? | Junior |
| 2 | What does the 'use client' directive actually do, and where should it be placed in the component tree? | Junior |
| 3 | How does Next.js distinguish between Static Rendering and Dynamic Rendering in the App Router? | Junior |
| 4 | How does loading.tsx implement streaming and instant loading states using React Suspense? | Junior |
| 5 | How does Incremental Static Regeneration (ISR) work in the App Router, and how do you configure revalidation? | Mid |
| 6 | What are Server Actions in Next.js, and how do they differ from Route Handlers? | Mid |
| 7 | How does data caching and request deduplication work in Next.js App Router data fetching? | Mid |
| 8 | How does error.tsx catch runtime exceptions, and why must it be a Client Component? | Mid |
| 9 | How does Next.js Middleware execute on the Edge runtime, and what are its performance constraints? | Senior |
| 10 | How does Partial Prerendering (PPR) combine static shell delivery with dynamic streaming? | Senior |
Q1. What is the fundamental difference between Server Components and Client Components in Next.js?
Level: Junior
What the interviewer is checking: The interviewer wants to verify that you understand the React Server Components mental model, code execution environments, and client bundle impact.
Short answer (say this first):
In the Next.js App Router, all components inside the app directory are React Server Components by default. Server Components execute exclusively on the server, never download JavaScript to the client bundle, and can access server resources directly. Client Components (declared with 'use client') are pre-rendered on the server to initial HTML and hydrated in the browser, enabling interactivity, browser APIs, and React hooks like useState and useEffect.
Detailed explanation:
- Execution Environment: Server Components run strictly during build time or request time on the Node.js/Edge server. They render to a serialized React Server Component (RSC) payload format, not HTML alone.
- Zero Client Bundle Impact: Heavy backend dependencies (like database drivers, markdown parsers, or date utilities) used inside Server Components remain on the server and never bloat client download size.
- Client Components & Hydration: Client Components still participate in server-side pre-rendering to output initial HTML for SEO and fast First Contentful Paint (FCP), after which React hydrates them in the browser.
Example:
// app/dashboard/page.tsx (Server Component by default)
import { db } from '@/lib/db';
export default async function DashboardPage() {
// Direct server/database access without an API route
const users = await db.user.findMany();
return (
<main>
<h1>Active Users: {users.length}</h1>
</main>
);
}
Common mistakes:
- Believing that Client Components render only in the browser. Client Components are also pre-rendered to static HTML on the server during initial load.
Likely follow-up questions:
- Can a Server Component be imported directly into a Client Component? — No, a Server Component cannot be imported into a Client Component directly; however, you can pass a Server Component as a
childrenprop into a Client Component.
Q2. What does the 'use client' directive actually do, and where should it be placed in the component tree?
Level: Junior
What the interviewer is checking: The interviewer wants to see if you understand boundary mechanics and component tree optimization rather than placing 'use client' at the top of every file.
Short answer (say this first):
The 'use client' directive serves as a boundary marker between the server-only module graph and the client-hydrated module graph. It tells Next.js and React that the file and all of its imported children should be packaged for client-side execution and hydration. Best practice is to push 'use client' down to the leaves of the component tree to maximize server-rendered content and minimize client JavaScript.
Detailed explanation:
- Boundary Semantics:
'use client'does not need to be declared on every component that needs interactivity; declaring it on a parent makes all nested imports part of the client bundle. - Leaves-First Pattern: Instead of marking an entire page or layout as
'use client', keep the page as a Server Component and extract only the interactive widget (e.g., an animated button, search bar, or modal) into a client component file. - Passing Server Content: You can nest Server Components inside Client Components by passing them as React
childrenor JSX props across the boundary slot.
Example:
// components/CounterButton.tsx (Leaf Client Component)
'use client';
import { useState } from 'react';
export function CounterButton() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Clicks: {count}</button>;
}
Common mistakes:
- Adding
'use client'at the top oflayout.tsxorpage.tsxjust to use a small hook, inadvertently converting the entire subtree into client-bundled code.
Likely follow-up questions:
- What happens if you use
windoworlocalStorageinside a Client Component? — They can only be accessed safely insideuseEffector event handlers; accessing them at the component top level throws a ReferenceError during server pre-rendering.
Q3. How does Next.js distinguish between Static Rendering and Dynamic Rendering in the App Router?
Level: Junior
What the interviewer is checking: The interviewer is confirming your understanding of build-time generation vs request-time execution and what triggers dynamic rendering.
Short answer (say this first):
In Next.js App Router, routes are statically rendered by default at build time and cached on a CDN. A route automatically switches to Dynamic Rendering (rendering per request on the server) as soon as Next.js detects dynamic functions—such as reading cookies(), headers(), or search params—or when an un-cached network request is encountered.
Detailed explanation:
- Static Rendering (Default): HTML and RSC payloads are generated during
next build. This yields the fastest TTFB because assets can be served immediately from a global CDN cache. - Dynamic Functions: Invoking
cookies(),headers(), or accessing thesearchParamsprop signals to Next.js that the content depends on incoming request data, triggering dynamic request-time rendering. - Route Segment Config: You can explicitly force or prevent dynamic behavior using
export const dynamic = 'force-dynamic'or'error'in page files.
Example:
// app/status/page.tsx (Dynamic Route due to request cookies)
import { cookies } from 'next/headers';
export default async function StatusPage() {
const cookieStore = await cookies();
const sessionToken = cookieStore.get('session');
return <div>Logged in with: {sessionToken ? 'Active Session' : 'Guest'}</div>;
}
Common mistakes:
- Assuming that making an asynchronous
fetchcall automatically makes a page dynamic. In Next.js, static routes can execute asynchronousfetchcalls during build time.
Likely follow-up questions:
- What is
generateStaticParamsused for? — It replaces the Pages router'sgetStaticPaths, allowing you to generate static routes at build time for dynamic route segments (e.g.,app/posts/[id]/page.tsx).
Q4. How does loading.tsx implement streaming and instant loading states using React Suspense?
Level: Junior
What the interviewer is checking: The interviewer wants to verify that you understand HTTP streaming, chunked transfer encoding, and how file-based routing integrates with React Suspense boundaries.
Short answer (say this first):
loading.tsx is a special Next.js file convention that automatically wraps the sibling page.tsx component in a React <Suspense fallback={<Loading />}> boundary. When a user navigates to the route, Next.js instantly streams the static layout and fallback UI over the HTTP connection, and then streams in the page content chunk once asynchronous data fetching finishes on the server.
Detailed explanation:
- Automated Suspense: Behind the scenes, Next.js transforms
layout.tsx+page.tsx+loading.tsxinto a nested component tree where the page is placed inside a Suspense boundary withloading.tsxas its fallback. - Progressive Hydration: The browser paints the skeleton UI immediately without waiting for slow database or external API calls, significantly reducing Time to First Byte (TTFB) and Largest Contentful Paint (LCP).
- Granular Suspense: While
loading.tsxwraps the whole page, you can place manual<Suspense>boundaries around individual slow widgets inside the page for granular, independent component streaming.
Example:
// app/products/loading.tsx
export default function ProductsLoading() {
return <div className="skeleton-grid">Loading product catalog...</div>;
}
// app/products/page.tsx
export default async function ProductsPage() {
const products = await fetchProducts(); // Slow backend call
return <ProductList items={products} />;
}
Common mistakes:
- Thinking that
loading.tsxrequires client-side JavaScript to render. The fallback HTML is streamed directly from the server.
Likely follow-up questions:
- Can layouts re-render when switching between sibling routes with
loading.tsx? — No, layouts remain preserved and interactive; only the inner page segment within the Suspense boundary switches to the loading state.
Q5. How does Incremental Static Regeneration (ISR) work in the App Router, and how do you configure revalidation?
Level: Mid
What the interviewer is checking: The interviewer is probing your knowledge of scalable cache invalidation, hybrid rendering, and on-demand vs time-based content refreshment.
Short answer (say this first):
Incremental Static Regeneration allows you to update static pages in the background after the site has been built without rebuilding the entire application. In the App Router, you configure time-based ISR using fetch(url, { next: { revalidate: 60 } }) or export const revalidate = 60, which serves cached static HTML to users while triggering a background re-render once the interval expires.
Detailed explanation:
- Time-Based Revalidation: When a request arrives after the revalidation interval (e.g., 60 seconds), the cached page is served immediately (stale-while-revalidate), while Next.js triggers a regeneration on the server. If regeneration succeeds, the cache is updated for subsequent requests.
- On-Demand Revalidation: When data changes instantly (such as a CMS webhook or form submission), you can purge the cache immediately using
revalidatePath('/posts')orrevalidateTag('products')from inside a Server Action or Route Handler. - Cache Tags: Tagging requests (
fetch(url, { next: { tags: ['posts'] } })) enables precise, fine-grained invalidation without purging entire pages.
Example:
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // Time-based: Revalidate once per hour
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await fetch(`https://api.cms.com/posts/${slug}`, {
next: { tags: [`post-${slug}`] },
}).then((res) => res.json());
return <article><h1>{post.title}</h1><p>{post.body}</p></article>;
}
Common mistakes:
- Assuming that a revalidation interval of 60 seconds means the page regenerates automatically every 60 seconds. Regeneration is lazy and only triggers when a user actually requests the page after the timer expires.
Likely follow-up questions:
- How does
revalidateTag()differ fromrevalidatePath()? —revalidateTag()purges all cached fetch queries sharing that tag across any page in the app, whereasrevalidatePath()invalidates the cached route segment and its layouts.
Q6. What are Server Actions in Next.js, and how do they differ from Route Handlers?
Level: Mid
What the interviewer is checking: The interviewer wants to assess whether you understand modern RPC-style data mutation, progressive enhancement, and when to use Server Actions over traditional REST API endpoints.
Short answer (say this first):
Server Actions are asynchronous functions declared with the 'use server' directive that execute on the server and can be invoked directly from Client or Server Components without manually creating REST endpoints. Route Handlers (route.ts) are traditional HTTP endpoints providing standard REST methods (GET, POST, PUT, DELETE) for external consumers or Webhooks. Server Actions are designed primarily for form submissions and UI-driven data mutations.
Detailed explanation:
- Direct Invocation: Server Actions eliminate the boilerplate of creating an API route, writing
fetch('/api/...'), and handling request parsing; you call the server function like a local async function. - Progressive Enhancement: When passed to an HTML
<form action={myAction}>, Server Actions can submit and execute even before client JavaScript has loaded or hydrated in the browser. - Integrated Revalidation: Server Actions seamlessly pair with
revalidatePath(),revalidateTag(), andredirect(), allowing state updates and cache purges within a single network roundtrip.
Example:
// app/actions.ts
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function addComment(formData: FormData) {
const text = formData.get('comment') as string;
await db.comment.create({ data: { text } });
revalidatePath('/comments');
}
Common mistakes:
- Thinking
'use server'creates a Server Component.'use server'marks a callable server function entry point, whereas Server Components are default files without directives.
Likely follow-up questions:
- How do you handle pending states with Server Actions in React? — Use React 19's
useActionStatehook oruseTransitionto track pending execution, returned errors, and optimistic UI updates.
Q7. How does data caching and request deduplication work in Next.js App Router data fetching?
Level: Mid
What the interviewer is checking: The interviewer is testing your depth of knowledge regarding Next.js's multi-layered caching architecture and how duplicate requests are resolved during rendering.
Short answer (say this first):
Next.js extends the native Web fetch API to provide automatic request memoization and persistent data caching. During a single server render pass, multiple components calling the exact same fetch endpoint with identical URLs and options are automatically deduplicated into a single request. In Next.js 15, fetch requests default to un-cached (no-store), but can be explicitly cached using { cache: 'force-cache' } or the 'use cache' directive.
Detailed explanation:
- Request Memoization: Handled in React's component tree lifecycle. If three nested components call
fetch('/api/user'), the network request fires once, and the result is reused across the render tree. - Data Cache (Persistent): Persists across requests and deployments. Controls whether responses are stored in the server cache or fetched fresh from the origin on every request.
- Next.js 15 Shift: In Next.js 14,
fetchdefaulted toforce-cache. Next.js 15 aligned with developer expectations by defaultingfetchtono-store(uncached) unless caching is explicitly opted into.
Example:
// Nested components calling this simultaneously only trigger ONE network request
async function getUser() {
const res = await fetch('https://api.example.com/user', {
cache: 'force-cache', // Explicitly cache across requests
});
return res.json();
}
Common mistakes:
- Expecting request memoization to persist across multiple HTTP requests from different users. Request memoization is scoped strictly to a single render pass.
Likely follow-up questions:
- How can you deduplicate non-fetch asynchronous functions (e.g., ORM or database queries)? — Wrap the query function in React's
cache()utility fromreactto memoize results for the duration of the render.
Q8. How does error.tsx catch runtime exceptions, and why must it be a Client Component?
Level: Mid
What the interviewer is checking: The interviewer wants to verify that you understand error isolation, React Error Boundaries, and why recovery mechanisms require client-side execution.
Short answer (say this first):
error.tsx is a file convention that automatically wraps its route segment in a React Error Boundary. It catches runtime errors thrown by nested Server or Client Components and renders a localized fallback UI instead of crashing the entire page. It must be a Client Component ('use client') because Error Boundaries require client-side React lifecycle methods to catch errors and execute recovery actions like reset().
Detailed explanation:
- Segment Isolation: An error in a nested
app/dashboard/analytics/error.tsxisolates the failure to the analytics box, keeping the outer dashboard layout and navigation fully interactive. - The
reset()Method: Next.js passes areset()callback toerror.tsx. Calling it prompts React to re-attempt rendering the failing segment. - Root Errors: To catch errors inside the root
app/layout.tsx, you must createapp/global-error.tsx, which replaces the entire root HTML document when triggered.
Example:
// app/dashboard/error.tsx
'use client';
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div role="alert">
<h2>Failed to load dashboard data</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try Again</button>
</div>
);
}
Common mistakes:
- Attempting to declare
error.tsxas a Server Component. Next.js will throw a build error because Error Boundaries require client runtime state.
Likely follow-up questions:
- Does
error.tsxcatch errors thrown inside its siblinglayout.tsx? — No,error.tsxonly catches errors thrown inside its child route segments; to catch errors in a layout, place the error boundary in the parent folder.
Q9. How does Next.js Middleware execute on the Edge runtime, and what are its performance constraints?
Level: Senior
What the interviewer is checking: The interviewer is probing your architectural knowledge of Edge infrastructure, request routing lifecycles, and the computational limits of middleware.
Short answer (say this first):
Next.js Middleware (middleware.ts) runs before any request is processed by the App Router, executing on a lightweight Edge runtime based on V8 isolates rather than a full Node.js environment. It is designed for low-latency operations such as authentication verification, redirects, URL rewrites, and header manipulation. Because it intercepts all matching traffic, heavy computation or database querying in middleware degrades global Time to First Byte (TTFB).
Detailed explanation:
- Edge Runtime Characteristics: Middleware does not support native Node.js APIs (such as
fsor native TCP sockets) and enforces strict bundle size limits (typically under 1MB). - Matching Configuration: The
matcherconfig array ensures middleware only triggers on necessary routes, explicitly filtering out static assets (_next/static, images, and favicon). - Request Modification: Middleware can inspect incoming cookies/headers, rewrite routes internally using
NextResponse.rewrite(), redirect usingNextResponse.redirect(), or inject custom request headers down to Server Components.
Example:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const session = request.cookies.get('session_token');
if (!session && request.nextUrl.pathname.startsWith('/protected')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/protected/:path*'], // Exclude static files and public routes
};
Common mistakes:
- Performing slow database calls or heavy token cryptography inside middleware, which bottlenecks every incoming HTTP request.
Likely follow-up questions:
- How can you pass data from middleware into downstream Server Components? — You cannot pass arbitrary JavaScript objects; you must forward data via custom request headers (
request.headers.set('x-user-id', id)).
Q10. How does Partial Prerendering (PPR) combine static shell delivery with dynamic streaming?
Level: Senior
What the interviewer is checking: The interviewer is evaluating your awareness of the cutting edge of Next.js architecture (introduced in Next.js 14/15) and how static and dynamic rendering merge into a single model.
Short answer (say this first): Partial Prerendering (PPR) eliminates the historic binary choice between static and dynamic rendering. With PPR, Next.js generates a static HTML shell at build time containing the layout and fallback UI, while embedding dynamic component holes wrapped in React Suspense. When a request arrives, the static shell is served instantly from the CDN edge, and the dynamic holes stream into the same HTTP response as soon as they finish executing on the server.
Detailed explanation:
- The Binary Dilemma: Previously, adding a single dynamic call (
cookies()) forced the entire page to become dynamic, losing CDN static caching benefits. - The PPR Solution: During build time, Next.js prerenders the static page skeleton up to the Suspense boundaries.
- Single Request Delivery: Unlike traditional client-side fetching where the browser requests a shell and then fires follow-up AJAX requests, PPR delivers both the static shell and the streamed dynamic chunks through one continuous HTTP connection.
Example:
// app/store/page.tsx (With experimental PPR enabled)
import { Suspense } from 'react';
import { StaticProductInfo } from '@/components/StaticProductInfo';
import { DynamicPersonalizedPrice } from '@/components/DynamicPersonalizedPrice';
export default function StorePage() {
return (
<div>
{/* Prerendered statically at build time into CDN shell */}
<StaticProductInfo />
{/* Streamed dynamically on request through Suspense */}
<Suspense fallback={<div>Calculating member price...</div>}>
<DynamicPersonalizedPrice />
</Suspense>
</div>
);
}
Common mistakes:
- Confusing PPR with traditional client-side hydration or thinking it requires separate API endpoints for dynamic holes.
Likely follow-up questions:
- How does PPR impact caching infrastructure? — CDN edges can cache and immediately serve the prerendered static byte stream, keeping connection time minimal while the server streams the remainder.
Preparation Checklist
- Explain the boundary difference between Server Components and Client Components out loud in 3 sentences.
- Diagram where
'use client'should be positioned in a complex dashboard layout. - Understand the conditions that switch a Next.js route from static to dynamic rendering.
- Build a small sample with
loading.tsxand manual<Suspense>boundaries to verify progressive streaming. - Implement an on-demand revalidation Server Action using
revalidateTag(). - Compare Next.js 14 and Next.js 15 default fetch caching behaviors.
Conclusion
The Next.js App Router unifies client and server rendering under the React Server Components model. In an interview, success depends on demonstrating clarity about execution contexts: keeping heavy dependencies on the server, pushing interactivity to client leaf nodes, leveraging streaming Suspense for perceived performance, and using Server Actions for clean data mutations.