Master Next.js 14/15 interviews: App Router, Server Components, Server Actions, Turbopack, and caching - 25 questions with code examples.
This guide covers the 25 Next.js interview questions that actually get asked at product companies in 2026 - App Router architecture, Server Components, Server Actions, caching layers, Turbopack, and production patterns - each with a concise answer and a real code snippet.
The App Router isn't just a new folder structure. It's a fundamentally different mental model: React Server Components by default, nested layouts that don't remount, streaming with Suspense, and Server Actions that replace the need for most API endpoints. This guide covers the 25 questions that separate candidates who've read the docs from those who've shipped production App Router code.
Key Takeaways
The App Router (stable since Next.js 13.4) makes every component a Server Component by default - "use client" is the opt-in, not the default
Next.js 15 made
cookies(),headers(),params, andsearchParamsasync - this is the most common breaking change in 2026 interviewsFour distinct caching layers exist in the App Router: Request Memoization, Data Cache, Full Route Cache, and Router Cache
Server Actions are the preferred pattern for internal mutations; Route Handlers are for external API consumers
Turbopack (stable in dev as of Next.js 15, production builds stable in Next.js 15.5) delivers 76.7% faster local server startup (nextjs.org/blog/next-15)
Routing questions come first in almost every Next.js interview. Interviewers want to know whether you understand the architectural shift the App Router represents - not just that the folder is called app/ instead of pages/.
The Pages Router treats every file in pages/ as a Client Component by default, with data fetching via getStaticProps and getServerSideProps. The App Router, stable since Next.js 13.4, makes every component in app/ a React Server Component by default, uses native async/await for data fetching, and supports nested layouts, streaming, and Server Actions.
| Feature | Pages Router | App Router |
|---|---|---|
| Default component type | Client Component | Server Component |
| Data fetching | getStaticProps / getServerSideProps | async component + fetch() |
| Layouts | _app.js (re-renders on nav) | layout.tsx (persists across nav) |
| Streaming | Not supported | Built-in via Suspense |
| Server Actions | Not supported | "use server" directive |
Interview tip: Interviewers want the architectural shift - not just "different folder". Explain that the App Router moves data fetching to the server by default, reducing client JS bundle size and removing waterfall fetches.
In the App Router, the folder structure inside app/ defines the URL structure. A page.tsx file inside any folder is the publicly accessible route for that segment. Special filenames handle specific behaviors: layout.tsx wraps child routes, loading.tsx provides a Suspense boundary, error.tsx catches render errors, and not-found.tsx renders the 404 UI.
app/
├── layout.tsx → / (root layout, wraps everything)
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── layout.tsx → /blog (shared layout for all blog routes)
│ ├── page.tsx → /blog
│ ├── loading.tsx → auto Suspense fallback for /blog/*
│ ├── error.tsx → error boundary for /blog/*
│ └── [slug]/
│ └── page.tsx → /blog/:slugDynamic route segments use brackets in folder names. [slug] matches a single segment. [...slug] matches one or more segments (catch-all). [[...slug]] matches zero or more segments (optional catch-all). In Next.js 15, params is now async and must be awaited.
// app/blog/[slug]/page.tsx
// URL: /blog/my-post → params.slug = "my-post"
interface Props {
params: Promise<{ slug: string }>;
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params; // Next.js 15: params is async
const post = await fetchPost(slug);
return <article>{post.content}</article>;
}
// app/docs/[...slug]/page.tsx
// URL: /docs/a/b/c → params.slug = ["a", "b", "c"]
// app/shop/[[...filters]]/page.tsx
// URL: /shop OR /shop/shoes/red → both matchA layout.tsx file wraps its sibling page.tsx and all child routes. It persists across navigations in that segment - it does not remount when a child route changes. This is the critical difference from _app.js in the Pages Router, which re-rendered on every navigation.
// app/layout.tsx - root layout (required, wraps the entire app)
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
// app/dashboard/layout.tsx - nested layout, only active under /dashboard/*
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard-shell">
<Sidebar />
<main>{children}</main>
</div>
);
}Interview tip: Layouts don't rerender when the child route changes. This is what enables persistent sidebars and navigation without scroll position jumping.
loading.tsx is a shorthand for wrapping the page's content in a React boundary. Next.js renders loading.tsx immediately while the page's async data is resolving, then swaps in the real content. error.tsx is a React Error Boundary - it catches runtime errors in its segment and renders a recovery UI. It must be a Client Component.
// app/dashboard/loading.tsx - shown instantly, replaced when data resolves
export default function DashboardLoading() {
return <DashboardSkeleton />;
}
// app/dashboard/error.tsx - catches render errors in /dashboard segment
("use client");
interface ErrorProps {
error: Error;
reset: () => void;
}
export default function DashboardError({ error, reset }: ErrorProps) {
return (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}generateStaticParams replaces getStaticPaths from the Pages Router. It tells Next.js which dynamic route values to pre-render at build time. Paths not returned from generateStaticParams are either rejected (with dynamicParams = false) or rendered on demand and cached.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map((post) => ({
slug: post.slug, // pre-render /blog/my-post, /blog/other-post, etc.
}));
}
// Reject any slug not returned above
export const dynamicParams = false;generateMetadata is an async function exported from any page.tsx or layout.tsx that returns a Metadata object. It replaces the component from the Pages Router. Because it's async, you can fetch data to build metadata for dynamic routes, including Open Graph and Twitter card images.
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
interface Props {
params: Promise<{ slug: string }>;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await fetchPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.coverImage, width: 1200, height: 630 }],
},
};
}Route Groups use a (groupName) folder convention. The folder name is excluded from the URL. They let you apply different layouts to routes at the same URL depth without affecting the URL structure. The most common pattern is separating a public marketing layout from a protected app layout.
app/
├── (marketing)/
│ ├── layout.tsx → marketing layout (full-width, landing style)
│ ├── page.tsx → /
│ └── pricing/
│ └── page.tsx → /pricing
└── (app)/
├── layout.tsx → app layout (sidebar, auth required)
├── dashboard/
│ └── page.tsx → /dashboard
└── settings/
└── page.tsx → /settingsBoth /pricing and /dashboard live at the same URL depth, but use completely different layouts. No URL segments are added by the route group folders.
PERSONAL EXPERIENCE -
The caching model is where most developers get tripped up when migrating from the Pages Router. In practice, we've found that understanding which of the four caching layers applies to a given bug saves hours of debugging. Interviewers at product companies specifically probe this area because it's where shallow knowledge breaks down.
Server Components run only on the server. They have zero JavaScript footprint in the client bundle, can directly access databases, file systems, and secrets, and render to HTML before reaching the client. In the App Router, every component is a Server Component by default unless it has a "use client" directive.
// app/products/page.tsx - Server Component, no "use client"
// This entire component runs on the server. Zero JS sent to the browser.
import { db } from "@/lib/db";
export default async function ProductsPage() {
// Direct DB access - no API layer needed
const products = await db.product.findMany({ take: 20 });
return (
<ul>
{products.map((p) => (
<li key={p.id}>
<h2>{p.name}</h2>
<p>${p.price}</p>
{/* Client Component nested inside Server Component - valid */}
<AddToCartButton productId={p.id} />
</li>
))}
</ul>
);
}Add "use client" to a component when it needs useState, useEffect, event handlers, or browser-only APIs (window, localStorage, etc.). It marks a boundary: everything in that file and everything it imports runs on the client. The key discipline is pushing "use client" as far down the component tree as possible to keep most of the page as Server Components.
// components/AddToCartButton.tsx
"use client";
import { useState } from "react";
interface Props {
productId: string;
}
export function AddToCartButton({ productId }: Props) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)}>
{added ? "Added!" : "Add to Cart"}
</button>
);
}Interview tip: The most common mistake is marking an entire page "use client" because it has one interactive button. Push the boundary to the button component itself and keep the page as a Server Component.
Each strategy determines when HTML is generated and how fresh the data is. In the App Router, you don't call different functions - you control this through fetch() options.
| Strategy | HTML generated | Data freshness | Best for |
|---|---|---|---|
| SSG (Static) | Build time | Stale until redeploy | Blogs, docs, marketing |
| ISR (Incremental Static) | Build time + revalidation | Fresh every N seconds | Product pages, listings |
| SSR (Dynamic) | Per request | Always fresh | User dashboards, carts |
| CSR (Client) | Browser | Always fresh | Highly interactive UIs |
// SSG (default) - cached indefinitely
const data = await fetch("https://api.example.com/posts");
// ISR - revalidate every 60 seconds
const data = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 },
});
// SSR - never cache, always fetch fresh
const data = await fetch("https://api.example.com/posts", {
cache: "no-store",
});Data fetching in the App Router is just await fetch() inside an async Server Component. There's no getStaticProps, no getServerSideProps. You fetch data where you need it - which means multiple components can fetch their own data in parallel without prop drilling.
// app/dashboard/page.tsx
async function getUser() {
const res = await fetch("https://api.example.com/me", { cache: "no-store" });
return res.json();
}
async function getStats() {
const res = await fetch("https://api.example.com/stats", {
next: { revalidate: 300 },
});
return res.json();
}
export default async function DashboardPage() {
// Both fetches run in parallel - Next.js deduplicates identical URLs
const [user, stats] = await Promise.all([getUser(), getStats()]);
return (
<div>
<h1>Welcome, {user.name}</h1>
<StatsGrid stats={stats} />
</div>
);
}Server Actions are async functions marked with "use server" that run on the server but can be called directly from Client Components - including from form action attributes. They're the App Router's answer to mutations: type-safe, co-located with the UI, and automatically integrated with Next.js's revalidation system. In Next.js 15, Server Actions use unguessable IDs and dead code elimination for security.
// app/posts/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
await db.post.create({ data: { title } });
revalidatePath("/posts"); // bust the /posts page cache
}
// app/posts/new/page.tsx - use in a form action
import { createPost } from "../actions";
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" required />
<button type="submit">Create Post</button>
</form>
);
}revalidatePath purges the Full Route Cache for a specific URL. revalidateTag purges all cached fetch() calls that were tagged with a specific string, regardless of which URL they appear on. Use revalidateTag when the same data appears on multiple pages and you want a single invalidation to bust all of them.
// app/posts/page.tsx - tag the fetch so it can be invalidated by name
const data = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
});// app/posts/actions.ts - Server Action that busts all "posts"-tagged fetches
"use server";
import { revalidateTag, revalidatePath } from "next/cache";
export async function afterPostCreated() {
revalidateTag("posts"); // busts every fetch tagged "posts" across all pages
revalidatePath("/posts"); // also bust the /posts page shell from Full Route Cache
}Next.js sends the HTML shell (layout, static parts) to the browser immediately, then streams in the content of boundaries as their async data resolves. The user sees a real page with skeleton placeholders - not a blank screen. loading.tsx is shorthand for wrapping an entire page segment in .
// app/dashboard/page.tsx - slow component wrapped in Suspense
import { Suspense } from "react";
import { RevenueChart } from "./revenue-chart";
import { RecentOrders } from "./recent-orders";
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
{/* Renders immediately */}
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart /> {/* slow DB query - streams in later */}
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders /> {/* independent stream - doesn't block RevenueChart */}
</Suspense>
</div>
);
}Citation capsule: Next.js App Router has four distinct caching layers: Request Memoization (deduplicates identical
fetch()calls within a single render pass), Data Cache (persists fetched data across requests and deployments), Full Route Cache (stores rendered HTML and RSC payload on the server), and Router Cache (stores RSC payloads client-side for prefetched and visited routes). Each layer has different storage, duration, and invalidation mechanisms (Next.js Documentation, 2025).
| Cache Layer | What it stores | Duration | How to invalidate |
|---|---|---|---|
| Request Memoization | fetch() return values | Single request | Automatic - per render pass |
| Data Cache | fetch() responses | Persistent (across requests) | revalidatePath, revalidateTag, no-store |
| Full Route Cache | Rendered HTML + RSC payload | Persistent (at build) | Redeploy or revalidatePath |
| Router Cache | RSC payload (client) | Session (30s pages, 5min layouts) | router.refresh(), Next.js 15 sets page staleTime to 0 |
Interview tip: Most cache-related bugs live in the Data Cache or Full Route Cache. Knowing which layer is misbehaving narrows debugging from hours to minutes.
Next.js 15 introduced breaking changes to the async Request APIs and reversed several caching defaults. Every API that reads request-time data (cookies(), headers(), params, searchParams) is now async and must be awaited. GET Route Handlers and the Router Cache for Page segments are no longer cached by default.
// BEFORE (Next.js 14)
import { cookies } from "next/headers";
export async function GET() {
const cookieStore = cookies(); // sync - no await
const token = cookieStore.get("token");
return Response.json({ token });
}
// AFTER (Next.js 15) - must await
import { cookies } from "next/headers";
export async function GET() {
const cookieStore = await cookies(); // async - await required
const token = cookieStore.get("token");
return Response.json({ token });
}
// params in page components also changed:
// Before: { params }: { params: { slug: string } }
// After: { params }: { params: Promise<{ slug: string }> }
// Then: const { slug } = await params;"use cache" (Next.js 15 experimental, part of the dynamicIO flag) is a cache directive you can apply to any async function - not just fetch(). It lets you cache the result of a database query, an external API call, or any async computation. Use cacheTag() and cacheLife() inside the function for granular cache control. Note: the unstable_ prefix on both imports is required - these APIs haven't reached a stable interface and may change in minor versions.
// Cache any async function, not just fetch()
import {
unstable_cacheTag as cacheTag,
unstable_cacheLife as cacheLife,
} from "next/cache";
export async function getLatestPosts() {
"use cache";
cacheTag("posts");
cacheLife("hours"); // revalidate every hour
// This DB call is now cached like a fetch() with next.revalidate
return await db.post.findMany({ orderBy: { createdAt: "desc" }, take: 10 });
}The questions in this section are where mid-level candidates get filtered out. Parallel Routes and Intercepting Routes are almost never covered in tutorials, but they appear frequently in senior-level interviews because they reveal whether you understand Next.js as a routing framework - not just a React wrapper. PPR and the "use cache" directive signal whether a candidate tracks the Next.js roadmap.
Middleware runs at the Edge Runtime - before a request hits your server, on Vercel's CDN nodes globally. It matches routes via a matcher config and can rewrite, redirect, set headers, or return a response. Common uses: authentication redirects, A/B testing, locale detection, and header injection.
// middleware.ts (project root, next to app/)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("auth-token");
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*"],
};Interview tip: Middleware runs in the Edge Runtime - not Node.js. You can't use fs, crypto from Node, or most npm packages that require Node internals. Keep middleware thin: read cookies, check tokens, redirect or rewrite. Heavy logic belongs in Route Handlers or Server Actions. As of Next.js 15.5, Middleware can optionally run in the Node.js runtime via export const config = { runtime: 'nodejs' }, unlocking Node APIs - but this trades edge-CDN performance for Node.js compatibility.
Parallel Routes use @slot folders to render multiple pages in the same layout simultaneously, each with its own loading and error boundaries. A dashboard showing a main panel and a side panel - both with independent data and loading states - is the canonical example. Intercepting Routes use (.) conventions to render a route in a modal when navigated to from within the app, but as a full page on direct URL access.
// Parallel Routes: app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics, // @analytics slot
team, // @team slot
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div className="dashboard">
{children}
<aside>{analytics}</aside>
<aside>{team}</aside>
</div>
);
}
// app/dashboard/@analytics/page.tsx - independent loading + error boundaries
// app/dashboard/@team/page.tsx - fetches its own data independently
// Intercepting Routes: app/photos/(.)photos/[id]/page.tsx
// Navigating to /photos/42 from within the app → renders modal
// Direct visit to /photos/42 URL → renders full pageTurbopack is a Rust-based incremental bundler built by Vercel to replace Webpack in Next.js. It became stable for development in Next.js 15 and production builds became stable in Next.js 15.5. The official benchmarks from Vercel's production codebase are significant.
Citation capsule: Turbopack delivers 76.7% faster local server startup and 96.3% faster Fast Refresh on Vercel's own production codebase compared to Webpack. Next.js 15.5 extended this to production builds, delivering 2x-5x faster build times depending on project size (nextjs.org/blog/next-15, 2024).
# Enable Turbopack for dev (default in Next.js 15)
next dev --turbopack
# Production builds with Turbopack (Next.js 15.5+)
next build --turbopackTurbopack's speed comes from incremental computation: it only re-evaluates the modules that changed, not the entire dependency graph. The architecture is similar to how build systems like Bazel track dependencies.
Server Actions are for mutations within your own Next.js app: form submissions, button clicks, data updates. They're type-safe, co-located with your UI, and automatically handle revalidation. Route Handlers (app/api/.../route.ts) are for exposing an HTTP endpoint to external consumers: mobile apps, third-party webhooks, or any client outside your Next.js app.
// Server Action - internal mutation from a Client Component
"use server";
export async function updateProfile(data: FormData) {
await db.user.update({
where: { id: getSession().userId },
data: { name: data.get("name") },
});
revalidatePath("/profile");
}
// Route Handler - external REST endpoint
// app/api/webhooks/stripe/route.ts
export async function POST(request: Request) {
const event = await verifyStripeWebhook(request);
await handleStripeEvent(event);
return new Response("OK", { status: 200 });
}Interview tip: Prefer Server Actions over Route Handlers for anything your own UI triggers. Route Handlers add an HTTP layer that's unnecessary for same-app mutations and can't take advantage of Next.js's integrated revalidation.
PPR is an experimental Next.js rendering mode that combines a static HTML shell with dynamic streaming holes on a single page - served from a single request. The static shell (layout, nav, above-the-fold content) is served instantly from the CDN. Dynamic sections, defined by boundaries, stream in from the server. The goal: CDN speed for the shell, fresh data for dynamic parts.
// next.config.ts - enable PPR (experimental)
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: { ppr: true },
};
export default nextConfig;// app/product/[id]/page.tsx - PPR page
import { Suspense } from "react";
interface Props {
params: Promise<{ id: string }>;
}
export default async function ProductPage({ params }: Props) {
const { id } = await params; // Next.js 15: params is async
return (
<div>
<StaticHeader /> {/* in static shell - instant from CDN */}
<Suspense fallback={<PriceSkeleton />}>
<DynamicPrice productId={id} /> {/* dynamic hole - streams in */}
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<UserReviews productId={id} /> {/* dynamic hole - streams in */}
</Suspense>
</div>
);
}Authentication in the App Router uses three layers that each handle a different surface area. Middleware handles route protection at the edge before any server code runs. Server Components read the session to decide what to render. Server Actions handle login and logout mutations. Auth.js (formerly NextAuth) and Clerk both integrate with this three-layer pattern.
// Layer 1: Middleware - protect routes at the edge
// middleware.ts
export function middleware(request: NextRequest) {
const session = request.cookies.get("session");
if (!session && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
// Layer 2: Server Component - read session, pass to UI
// app/dashboard/page.tsx
import { getSession } from "@/lib/auth";
export default async function DashboardPage() {
const session = await getSession(); // reads cookies server-side
if (!session) redirect("/login");
return <Dashboard user={session.user} />;
}
// Layer 3: Server Action - handle logout
// app/actions/auth.ts
("use server");
import { deleteSession } from "@/lib/auth";
export async function logout() {
await deleteSession();
redirect("/login");
}Production performance in Next.js comes down to six concrete patterns. Each has a measurable impact on bundle size, Core Web Vitals, or server response time.
// 1. Push "use client" to leaf components - bad vs good
// BAD: entire page becomes client bundle
"use client";
export default function ProductPage() { ... }
// GOOD: only the interactive part is a client component
export default function ProductPage() {
return <div><StaticContent /><AddToCartButton /></div>; // AddToCartButton is "use client"
}
// 2. next/image with priority for LCP
import Image from "next/image";
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority />
// 3. next/font - zero layout shift from Google Fonts
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
// 4. revalidateTag instead of cache: "no-store" everywhere
// BAD: every visit hits your DB
const data = await fetch(url, { cache: "no-store" });
// GOOD: cached, busted only when data changes
const data = await fetch(url, { next: { tags: ["products"] } });
// 5. generateStaticParams for known dynamic routes
export async function generateStaticParams() {
return (await getTopProducts()).map((p) => ({ id: p.id }));
}
// 6. Suspense boundaries for slow data - don't block the page
<Suspense fallback={<Skeleton />}><SlowRecommendations /></Suspense>Yes, contextually. Companies with existing codebases - which is most companies - ask about getStaticProps, getServerSideProps, getInitialProps, and _app.js. Know them well enough to compare with App Router equivalents and explain the migration path. You won't need to write Pages Router code in a new role, but you'll inherit it.
next/navigation is the App Router package: it exports useRouter, usePathname, useSearchParams, and useParams. next/router is the Pages Router package. They're not interchangeable. Mixing them causes runtime errors. If you're in the app/ directory, always import from next/navigation.
Default to Server Component. Add "use client" only when you need useState, useEffect, event handlers, or browser APIs. The question to ask is: "Does this component need to respond to user interaction or browser state?" If no, keep it a Server Component. Push the "use client" boundary as far down the component tree as possible.
Focus on Next.js 14 and 15. From 14: stable Server Actions, the App Router caching model, and generateMetadata. From 15: async Request APIs (await cookies(), await params), reversed caching defaults (GET Route Handlers and Router Cache no longer cached by default), and Turbopack stable in dev. If the company runs Next.js 13, the concepts are the same - the APIs are nearly identical.
These 25 questions map the terrain from routing basics to production patterns. The App Router, Server Components, and Server Actions are the core - if you understand those three deeply, you can answer roughly 60% of what gets asked. The caching model (all four layers), the Next.js 15 breaking changes, and Turbopack's benchmarks are what separate good answers from great ones.
Work through the code examples until you can reproduce them without looking. Most interview questions that look architectural are really asking: "Can you write the code to do this?" The answer matters more than the theory.
Key Takeaways
App Router makes Server Components the default - "use client" is selective opt-in, not the starting point
Next.js 15's async Request APIs are the most tested breaking change - always
await cookies(),await paramsKnow all four caching layers by name and know how to invalidate each
Server Actions for internal mutations, Route Handlers for external API consumers
Turbopack: 76.7% faster dev startup, stable for production in Next.js 15.5 (nextjs.org/blog/next-15)
Ready to broaden your prep? Senior frontend interview topics 2026 covers system design, performance, and architecture questions asked at senior and staff-level roles. For the JavaScript fundamentals that every Next.js question builds on, see JavaScript interview questions 2026 and JavaScript coding interview questions 2026.