Next.js Interview Questions and Answers: 60 Questions from Easy to Hard
This guide is for Indian freshers and experienced developers preparing for Next.js interviews, from a first front-end or full-stack role to a senior position at a product company or an IT services firm. It has 60 questions in three levels: easy questions on routing, layouts, Server and Client Components and the special files; medium questions on data fetching, caching and revalidation, Server Actions, Route Handlers, middleware, metadata and deployment; and hard questions on caching internals, security, self-hosting, streaming and performance. Answers use the App Router of Next.js 14 and 15 and name the version wherever behaviour changed, especially the caching defaults in Next.js 15. The Pages Router appears only for comparison. Read each answer, rebuild the code in a fresh project, then explain it aloud.
- 60 questions, 3 levels
- Updated
- CoreHRx Jobs
- 119 open Next.js jobs
What Next.js interviews cover
- Routing in the App Router: folders and special files such as page, layout, loading, error and not-found, dynamic segments, route groups, and the Link component and navigation hooks.
- Server and Client Components: what runs where, when "use client" is needed, what can be passed across the boundary, and how hydration works.
- Data fetching, caching and revalidation, including how the defaults changed in Next.js 15 and how Next.js decides between static, dynamic and incremental rendering.
- Server Actions, Route Handlers and middleware: forms and mutations, public APIs and webhooks, and redirects for logged-out users.
- Production skills: images, fonts, metadata and SEO, environment variables, deployment with Node.js or Docker, and Core Web Vitals.
How to prepare as a fresher or an experienced developer
- Freshers: get React basics right first (props, state, hooks and effects), then build one App Router project with a list page, a detail page, a form that uses a Server Action, and loading and error states.
- Run next build and next start, not only next dev, and read the build output to see which routes are static and which are dynamic; many caching questions come from that difference.
- Find out which version the company uses, and learn the Next.js 15 changes by name: fetch and GET Route Handlers not cached by default, a shorter client Router Cache, async params, searchParams, cookies and headers, and React 19.
- Experienced candidates: prepare real stories, such as a stale-data bug you traced to one cache layer, a page you moved from client-side fetching to Server Components, or an app you scaled behind a load balancer.
- Revise the Pages Router only enough to compare it: getServerSideProps, getStaticProps, getStaticPaths, _app and API routes, and what replaces each one in the App Router.
Common mistakes and interview-day tips
- Saying that "use client" means a component renders only in the browser; Client Components are also rendered to HTML on the server on the first load.
- Describing Next.js 14 caching as if it were Next.js 15, or the other way round; say which version you mean before you explain a default.
- Treating middleware or a hidden button as security; every Server Action and Route Handler must check the user and the record itself.
- Marking whole pages or the root layout with "use client", which sends far more JavaScript to the browser than needed; keep client code in small leaf components.
- In a live coding round, say where each piece runs (server or browser), handle loading and error states, and test with a production build if there is time.
Next.js interview questions: Easy questions (freshers)
20 questions1. What is Next.js, and what does it add on top of React?
Next.js is a React framework for building complete web applications. React gives you components; Next.js adds the rest: file-based routing, rendering on the server at build time or per request, data fetching and caching, Server Actions and API endpoints, image and font optimisation, metadata for SEO, and a production build with code splitting. Teams choose it when pages must load fast and be found by search engines, such as a job portal or a shopping site. Since Next.js 13.4 the App Router, built on React Server Components, is the recommended way to build new apps.
2. What is the difference between the App Router and the Pages Router?
Both are routing systems in Next.js. The Pages Router, in the pages folder, is the older one: each file is a page, data comes from getServerSideProps or getStaticProps, and every component is a client component that is also server-rendered. The App Router, in the app folder and stable since Next.js 13.4, is built on React Server Components: components run on the server by default, layouts nest and keep their state, loading and error UI are files, and data is fetched in async components. New projects should use the App Router; both can live in one project during a migration.
3. How does file-based routing work in the App Router?
Each folder inside app is a URL segment, and a folder becomes a public route only when it contains a page file. So app/page.tsx is the home page, app/jobs/page.tsx is /jobs and app/jobs/saved/page.tsx is /jobs/saved. Other special files add behaviour to a segment: layout for shared UI, loading for a loading state, error for an error boundary, not-found for 404 UI and route for an API endpoint. Any other file, such as a component or a test, can sit in the same folder and is never served as a route. A common mistake is creating app/jobs.tsx and expecting /jobs to work.
4. How do you create a dynamic route, and how do you read the parameter in Next.js 15?
Put the folder name in square brackets. app/jobs/[id]/page.tsx matches /jobs/101 and /jobs/react-developer-pune, and the page receives the value in its params prop. In Next.js 15 params is a Promise, so you await it in an async Server Component; in Next.js 14 it was a plain object. A catch-all folder [...slug] matches one or more segments and gives an array, and an optional catch-all [[...slug]] also matches the path with no extra segments. Values arrive as strings, or arrays of strings for catch-alls, so convert and validate them before you use them in a query.
// app/jobs/[id]/page.tsx (Next.js 15)
export default async function JobPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const job = await getJob(Number(id));
return <h1>{job.title}</h1>;
}5. What is a layout, and what must the root layout contain?
A layout is UI shared by a segment and everything below it, such as a header, sidebar or footer. It receives the page or nested layout as the children prop. Layouts nest: the root layout wraps every page, and app/dashboard/layout.tsx wraps only the dashboard pages. When you navigate between pages that share a layout, the layout is not re-rendered and keeps its state, so an open menu or a sidebar scroll position survives. The root layout is required and must render the html and body tags. For UI that should remount on every navigation, use a template file instead.
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>Pune Jobs</header>
{children}
</body>
</html>
);
}6. What is a Server Component, and what can it not do?
In the App Router every component is a Server Component unless it is in a file marked "use client" or imported from one. Server Components run only on the server, can be async, and can read a database, files or secret keys directly. Their own JavaScript is never sent to the browser, which keeps the bundle small. The trade-off is that they cannot use state or effects such as useState and useEffect, event handlers such as onClick, or browser APIs such as window and localStorage. For anything interactive, render a small Client Component inside the Server Component.
// app/jobs/page.tsx - a Server Component
import { db } from "@/lib/db";
export default async function JobsPage() {
const jobs = await db.job.findMany({ take: 20 });
return (
<ul>
{jobs.map((job) => (
<li key={job.id}>{job.title}</li>
))}
</ul>
);
}7. When do you need the "use client" directive, and where do you put it?
Put "use client" at the very top of a file, before the imports, when its component needs interactivity or browser features: useState, useEffect and other hooks, event handlers, browser APIs, or a library that depends on them. It marks a boundary: that module and everything it imports become part of the client bundle, so you do not repeat it in every file below. A common mistake is adding it to a whole page just to make one button work, which sends the entire page to the browser. Move the button into its own small Client Component instead.
"use client";
import { useState } from "react";
export default function SaveJobButton() {
const [saved, setSaved] = useState(false);
return (
<button onClick={() => setSaved(!saved)}>
{saved ? "Saved" : "Save job"}
</button>
);
}8. Is a Client Component rendered only in the browser?
No. On the first page load, Client Components are also rendered to HTML on the server, so users and search engines see content before any JavaScript runs. The browser then downloads the component code and hydrates the HTML, which means React attaches the event handlers and makes it interactive. On later client-side navigations, Client Components render in the browser only. This is why reading window or localStorage at the top of a Client Component fails with window is not defined: that code also ran on the server. Read such values in useEffect or an event handler.
"use client";
import { useEffect, useState } from "react";
export function CityBanner() {
const [city, setCity] = useState("");
useEffect(() => {
// localStorage exists only in the browser
setCity(localStorage.getItem("city") ?? "");
}, []);
return city ? <p>Jobs in {city}</p> : null;
}9. How do you navigate between pages in the App Router?
Use the Link component from next/link for ordinary links. It renders an anchor tag, which is good for accessibility and SEO, but navigates on the client without a full reload and keeps shared layouts mounted. In production it also prefetches routes as they come into view. For navigation in code, such as after a form submit, use useRouter from next/navigation and call push or replace; usePathname and useSearchParams read the current URL. These hooks work only in Client Components. A common mistake is importing useRouter from next/router, the Pages Router version, which fails in the app folder.
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
export function Nav() {
const pathname = usePathname();
const router = useRouter();
return (
<nav>
<Link
href="/jobs"
className={pathname === "/jobs" ? "active" : ""}
>
Jobs
</Link>
<button onClick={() => router.push("/login")}>Login</button>
</nav>
);
}10. Explain SSR, SSG, ISR and CSR in Next.js terms.
SSG, static site generation, renders a page once at build and serves the same HTML to everyone; it is the fastest. SSR, server-side rendering, renders on the server per request, needed when the output depends on the request, such as a cookie or a search query. ISR, incremental static regeneration, is static rendering that refreshes after a set time or on demand, so a job listing stays fast and still updates. CSR, client-side rendering, fetches data in the browser. In the App Router there is no special function for each: Next.js decides per route from the APIs and cache options you use.
11. How do you fetch data for a page in the App Router?
Make the page an async Server Component and await the data directly, with fetch or your database client. There is no need for useEffect, getServerSideProps or an API route for your own data, and you should not call your own Route Handler from a Server Component, because that adds an extra network hop. Caching depends on the version: in Next.js 14 fetch results are cached by default, while in Next.js 15 they are not cached unless you pass cache: "force-cache" or a revalidate time. Always check res.ok, because fetch does not throw on a 404 or 500 response.
// app/jobs/page.tsx
export default async function JobsPage() {
const res = await fetch("https://api.example.com/jobs", {
next: { revalidate: 300 }, // cache for 5 minutes
});
if (!res.ok) throw new Error("Could not load jobs");
const jobs: { id: number; title: string }[] = await res.json();
return <JobList jobs={jobs} />;
}12. What does a loading file do?
A loading.tsx file in a route folder shows instant loading UI while that segment's page is rendered on the server. Next.js wraps the page in a React Suspense boundary and uses your component as the fallback, so on navigation the user sees the shared layout and a skeleton straight away, and the real content replaces it when it is ready. The layout stays interactive meanwhile. For dynamic routes the loading UI is prefetched as well, so navigation feels immediate. Keep the skeleton about the same size as the real content, so the page does not jump when the content arrives.
13. How does an error file work, and why must it be a Client Component?
An error.tsx file wraps a route segment in a React error boundary. If the page or anything below it throws while rendering, Next.js shows your error component in its place while the layout above keeps working. It must be a Client Component because error boundaries are a client-side React feature. It receives two props: error, and reset, which tries to render the segment again. An error.tsx does not catch errors from the layout in the same folder, because that layout sits above the boundary; those go to the parent segment's error file, and errors in the root layout need global-error.tsx.
// app/jobs/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div role="alert">
<p>Could not load jobs. Please try again.</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}14. How do you show a 404 page for a record that does not exist?
Call notFound() from next/navigation when the data is missing. It stops rendering, shows the nearest not-found.tsx file, and sets a 404 status code as long as the response has not started streaming. The root app/not-found.tsx also handles every URL that matches no route. This is better than rendering an empty page, which search engines may index as a thin page with a 200 status. A common mistake is returning null or a line of text such as Job not found from the page, which looks fine to users but sends the wrong status code to crawlers and monitoring tools.
15. Why use next/image instead of a plain img tag?
The Image component optimises images for you. It serves resized copies for different screen widths, converts them to a modern format such as WebP, lazy-loads images below the fold, and needs width and height or the fill prop, so the browser reserves space and the layout does not shift. For a local image imported as a module, the size and a blur placeholder come from the file itself. Add the priority prop to the main image above the fold, usually the LCP element, so it loads early. Images from other domains must be allowed in next.config with remotePatterns.
16. What does next/font do, and why is it better than linking a font stylesheet?
next/font downloads font files at build time and serves them from your own domain along with the other static assets, so the browser sends no request to Google Fonts. That saves an extra connection, helps privacy, and lets the font be cached with your app. It also creates a fallback font with adjusted metrics, which greatly reduces layout shift when the real font arrives. Use next/font/google for Google Fonts and next/font/local for your own files. Create each font once, in the root layout or a shared module, and reuse its class name or CSS variable everywhere.
17. How do you set the page title and description in the App Router?
Export a metadata object from a layout or page, or a generateMetadata function when the values depend on data. Next.js turns it into title, meta and Open Graph tags in the head, merging values from the root layout down to the page. A title template in the root layout lets each page set a short title and get a consistent suffix. Metadata exports work only in Server Components. Do not write title or meta tags by hand in the root layout. Since Next.js 14, viewport settings and the theme colour go in a separate viewport export, not in metadata.
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: { template: "%s | Pune Jobs", default: "Pune Jobs" },
description: "New IT jobs in Pune every day.",
};
// app/about/page.tsx
export const metadata = { title: "About" };
// rendered title: About | Pune Jobs18. How do environment variables work in Next.js?
Next.js loads .env files into process.env for you: .env for defaults, .env.development or .env.production for one environment, and .env.local for values on your machine that override them and are never committed. By default a variable is available only on the server, which is right for database URLs and API secrets. To use a value in the browser, give it the NEXT_PUBLIC_ prefix, and Next.js copies its value into the JavaScript bundle at build time. So never give a secret that prefix, and remember that changing a public value needs a new build.
19. What is a Route Handler?
A Route Handler is an API endpoint in the App Router. You create a route.ts file in a folder and export functions named after HTTP methods, such as GET and POST. They use the standard Web Request and Response objects, and NextRequest and NextResponse add helpers for cookies and URLs. A folder can have a page file or a route file, not both. Route Handlers replace the pages/api routes of the Pages Router. Caching differs by version: in Next.js 14 a GET handler that does not read the request is cached by default, while in Next.js 15 GET handlers are dynamic unless you opt in.
// app/api/jobs/route.ts -> /api/jobs?city=Pune
import { NextResponse, type NextRequest } from "next/server";
export async function GET(request: NextRequest) {
const city =
request.nextUrl.searchParams.get("city") ?? "Bengaluru";
const jobs = await findJobsByCity(city);
return NextResponse.json({ city, jobs });
}20. What is a Server Action?
A Server Action is an async function that runs on the server and can be called from your UI, most often as the action of a form. You mark it with "use server", either at the top of the function body inside a Server Component or at the top of a separate file so Client Components can import it. Next.js sends a POST request when the form is submitted, and a form in a Server Component works even before JavaScript has loaded. Server Actions are stable since Next.js 14. Treat each one like a public API endpoint: validate the input and check the user inside the action every time.
// app/contact/page.tsx
export default function ContactPage() {
async function sendEnquiry(formData: FormData) {
"use server";
const email = String(formData.get("email") ?? "");
await saveEnquiry(email);
}
return (
<form action={sendEnquiry}>
<input name="email" type="email" required />
<button type="submit">Send</button>
</form>
);
}Ready to check yourself? Take the free Next.js mock test
Next.js interview questions: Medium questions (1-3 years)
20 questions21. Which caching defaults changed in Next.js 15 compared with Next.js 14?
Three defaults changed. First, fetch requests are no longer cached by default; in Next.js 14 they were stored in the Data Cache unless you opted out, and now you opt in with cache: "force-cache" or a revalidate time. Second, GET Route Handlers are no longer cached by default; export dynamic = "force-static" to cache one. Third, the client Router Cache no longer reuses dynamic pages on navigation (a stale time of 0 instead of 30 seconds), though layouts and back and forward navigation still use it. Also, params, searchParams, cookies and headers became async.
22. How does time-based revalidation (ISR) work in the App Router?
Set a revalidate time on a fetch with next: { revalidate: 600 }, or for the whole segment with export const revalidate = 600. For those 600 seconds everyone gets the cached result. The first request after that still gets the old page and triggers a regeneration in the background; later requests get the new one. If regeneration fails, the last good version keeps being served. When a route has several revalidate values, the lowest decides how often it regenerates. The segment value must be a literal, so 600 works but 60 * 10 does not, and it is not a timer: with no traffic, nothing regenerates.
// One fetch: refresh at most every 10 minutes
const res = await fetch("https://api.example.com/jobs", {
next: { revalidate: 600 },
});
// Whole route segment (page.tsx or layout.tsx)
export const revalidate = 600;23. How do you refresh cached pages as soon as content changes, for example from a CMS webhook?
Use on-demand revalidation. Tag the data when you fetch it, such as next: { tags: ["jobs"] }, then call revalidateTag("jobs") when it changes: every cached fetch with that tag, on any page, is marked stale and fetched again on the next visit. revalidatePath("/jobs") does the same for one path. Call them in a Server Action after a write, or in a Route Handler that the CMS calls as a webhook. Protect that endpoint with a secret kept in an environment variable, or anyone can flush your cache. Tags usually beat paths, because one record can appear on many pages.
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
export async function POST(request: Request) {
const secret = request.headers.get("x-webhook-secret");
if (secret !== process.env.CMS_WEBHOOK_SECRET) {
return Response.json({ ok: false }, { status: 401 });
}
revalidateTag("jobs");
return Response.json({ ok: true });
}
// where the data is read:
// fetch(url, { next: { tags: ["jobs"] } })24. What is generateStaticParams, and what does dynamicParams control?
generateStaticParams runs at build time in a dynamic route and returns the params to prerender; it replaces getStaticPaths from the Pages Router. For a city page you might return the twenty biggest cities so they are ready as static HTML. dynamicParams decides what happens for a value not in that list: true, the default, renders it on the first request and caches it like the others, while false returns a 404. With thousands of pages, prerender only the most visited and let the rest render on demand, which keeps builds short. Add a revalidate time if the data changes.
// app/jobs/in/[city]/page.tsx
export async function generateStaticParams() {
const cities = await getTopCities(20);
return cities.map((c) => ({ city: c.slug }));
}
export const dynamicParams = true; // the default
export const revalidate = 3600; // refresh hourly25. What makes a route dynamic instead of static?
A route renders at request time when anything in its tree needs request data. In Next.js 15 that means calling cookies(), headers(), draftMode() or connection(), reading the searchParams prop, a fetch with cache: "no-store", or exporting dynamic = "force-dynamic" or revalidate = 0. Next.js 14 is similar, with unstable_noStore in place of connection. One such call anywhere in the route makes the whole route dynamic, so reading a cookie in the root layout makes every page dynamic. Check the next build output, and keep request-specific reads out of shared layouts.
// app/dashboard/page.tsx (Next.js 15)
import { cookies } from "next/headers";
export default async function Dashboard() {
const cookieStore = await cookies(); // makes the route dynamic
const city = cookieStore.get("city")?.value ?? "Delhi";
return <p>Jobs near {city}</p>;
}26. How does streaming with Suspense improve a slow page?
Without streaming, the server waits for all the data before sending any HTML, so one slow query delays the whole page. With Suspense boundaries, Next.js sends the HTML for the ready parts first, with fallbacks for the rest, and streams each delayed section in the same response as its data arrives. The user sees the job title and apply button at once while similar jobs load. A loading file puts the whole page in one boundary; Suspense inside the page gives finer control. The slow async component must sit inside the boundary: awaiting its data in the page itself blocks everything again.
import { Suspense } from "react";
export default async function JobPage({ params }) {
const { id } = await params;
return (
<main>
<JobHeader id={id} />
<Suspense fallback={<p>Loading similar jobs...</p>}>
<SimilarJobs id={id} />
</Suspense>
</main>
);
}27. What is a data-fetching waterfall in Server Components, and how do you avoid it?
A waterfall happens when requests run one after another although they do not depend on each other, such as two awaits in a row in a page, or a child that starts its fetch only after its parent has finished. The page then takes the sum of all the times. Start independent requests together with Promise.all, or give each slow part its own Suspense boundary so they load in parallel and stream separately. Sequential fetching is still right when the second call needs the first result. Promise.all rejects if any request fails, so use Promise.allSettled for optional data.
export default async function Profile({ params }) {
const { id } = await params;
// Both requests start at the same time
const [candidate, applications] = await Promise.all([
getCandidate(id),
getApplications(id),
]);
return <ProfileView c={candidate} apps={applications} />;
}28. How do you show validation errors and a pending state for a form that uses a Server Action?
Return errors from the action as data instead of throwing, and read them with the useActionState hook, used with React 19 in Next.js 15; in Next.js 14 the hook was useFormState from react-dom. The action then receives the previous state and the form data, and the hook returns the current state, a wrapped action for the form and a pending flag. Validate on the server, for example with Zod, because browser checks can be skipped. For a submit button in its own component, useFormStatus from react-dom gives the status of its parent form. Show errors in an aria-live region.
"use client";
import { useActionState } from "react";
import { applyForJob } from "./actions"; // a "use server" file
export function ApplyForm({ jobId }: { jobId: string }) {
const [state, formAction, pending] = useActionState(
applyForJob,
{ message: "" }
);
return (
<form action={formAction}>
<input type="hidden" name="jobId" value={jobId} />
<input name="mobile" inputMode="numeric" required />
<button disabled={pending}>Apply</button>
<p aria-live="polite">{state.message}</p>
</form>
);
}29. Why does redirect() inside a try/catch block in a Server Action not work as expected?
redirect() from next/navigation works by throwing a special error that Next.js catches to perform the redirect. If you call it inside a try block, your own catch receives that error instead, and the redirect is swallowed or logged as a failure. Do the risky work inside try/catch, return an error state from the catch, and call redirect after the block. notFound() works the same way. Call revalidatePath or revalidateTag before redirecting, so the list page shows the new record. By default redirect uses status 307, and 303 when it is called from a Server Action.
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createJob(formData: FormData) {
const employer = await requireEmployer();
let id: number;
try {
id = await insertJob(employer.orgId, formData);
} catch {
return { error: "Could not save the job." };
}
revalidatePath("/jobs");
redirect(`/jobs/${id}`); // outside try/catch
}30. When would you use a Route Handler instead of a Server Action?
Use Server Actions for changes started from your own Next.js UI: submitting a form, saving a job, updating a profile. They are called like typed functions, work with progressive enhancement, and can revalidate data and return fresh UI in one round trip. Use Route Handlers when something outside your React UI calls the server: a mobile app, a partner integration, a payment gateway webhook, a CSV download, or any endpoint that needs GET, custom headers or CORS. Server Actions always use POST and are not meant for fetching data, since the client runs them one at a time. Both need the same checks.
31. How do you protect a group of pages with middleware?
Create middleware.ts in the project root, or in src, and export a middleware function and a config with a matcher, so it runs only for the paths you list. Middleware runs before the route is rendered and before cached content is served, which makes it a good place for a quick redirect: if the session cookie is missing, send the user to /login and keep the original path. By default it runs on the Edge runtime, so keep it light: read cookies and headers, but avoid database queries. The matcher must be a constant the build can read. Treat this check as a convenience, not your only security layer.
// middleware.ts
import { NextResponse, type NextRequest } from "next/server";
export function middleware(request: NextRequest) {
if (!request.cookies.has("session")) {
const url = new URL("/login", request.url);
url.searchParams.set("next", request.nextUrl.pathname);
return NextResponse.redirect(url);
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/account/:path*"],
};32. What are route groups and private folders, and when do you use them?
A folder in parentheses, such as (marketing) or (dashboard), is a route group: it organises routes without adding anything to the URL. Its main use is giving sections their own layouts, for example a public header for marketing pages and a sidebar for logged-in pages, while the URLs stay /pricing and /jobs. Two groups must not resolve to the same URL, or the build fails. You can even give each group its own root layout, but moving between them then causes a full page load. A folder that starts with an underscore, such as _components, is private and never becomes a route.
33. How can a Client Component show a Server Component inside it?
It cannot import one, because everything imported into a "use client" file becomes client code, so the Server Component would lose its server-only abilities. Instead, a parent Server Component passes the Server Component to the Client Component as children or another prop. The server renders it first, and the Client Component receives the finished result as a slot. This keeps a drawer, tabs or modal interactive while their content still fetches data on the server. Props that cross from server to client must be serialisable, so pass data, not functions, apart from Server Actions.
// app/jobs/page.tsx - Server Component
import FilterDrawer from "./FilterDrawer"; // "use client"
import JobFilters from "./JobFilters"; // Server Component
export default function JobsPage() {
return (
<FilterDrawer>
<JobFilters />
</FilterDrawer>
);
}34. How do you use React Context, such as a theme provider, in the App Router?
Context needs createContext and hooks, which work only in Client Components, so put the provider in its own file marked "use client" and render it in the root layout around children. The layout stays a Server Component, and pages passed in as children stay Server Components too; only components that call useContext must be client. Server Components cannot read context, so pass server data as props or fetch it where it is needed. Render providers as deep as you can, around children rather than the whole document, and never turn the root layout into a Client Component just to add a provider.
35. What causes hydration errors in Next.js, and how do you fix them?
A hydration error means the server HTML differs from React's first render in the browser. Common causes are dates and times (the server may run in UTC while users are in IST), Math.random or new Date() during render, branches on typeof window, reading localStorage while rendering, invalid nesting such as a div inside a p, and browser extensions that change the page. Fixes: compute browser-only values in useEffect, format dates with a fixed time zone such as Asia/Kolkata, correct the nesting, or load a browser-only widget with next/dynamic and ssr: false from a Client Component.
"use client";
import { useEffect, useState } from "react";
export function PostedAgo({ postedAt }: { postedAt: string }) {
const [label, setLabel] = useState("");
useEffect(() => {
// Date.now() differs between server and browser,
// so compute it only after hydration
const ms = Date.now() - new Date(postedAt).getTime();
setLabel(Math.floor(ms / 86400000) + " days ago");
}, [postedAt]);
return <span>{label}</span>;
}36. In Next.js 15, why are params and searchParams Promises, and how do you use them?
Next.js 15 made the request-specific APIs async: the params and searchParams props, and cookies(), headers() and draftMode(). This lets Next.js start rendering the parts of a page that do not need request data before those values are read. In a Server Component, await them; in a Client Component, unwrap the prop with React's use() or read the URL with useParams and useSearchParams. Synchronous access still works in 15 to ease migration, but it is deprecated and logs warnings, and the official codemod updates most call sites. Reading searchParams makes the page dynamic.
// app/search/page.tsx (Next.js 15)
type Search = { q?: string; page?: string };
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<Search>;
}) {
const { q = "", page = "1" } = await searchParams;
const results = await searchJobs(q, Number(page) || 1);
return <Results items={results} />;
}37. How do you handle SEO for thousands of dynamic job pages?
Use generateMetadata in the dynamic page to build a unique title, description, canonical URL and Open Graph data from each record. fetch calls with the same URL and options are shared between generateMetadata and the page within one request; for database calls, wrap the loader in React's cache so it runs once. Set metadataBase in the root layout so relative URLs become absolute. Add app/sitemap.ts and app/robots.ts, and use generateSitemaps when one sitemap would pass 50,000 URLs. Call notFound() for removed jobs, and keep the main content in server-rendered HTML.
// app/jobs/[id]/page.tsx (Next.js 15)
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>;
}): Promise<Metadata> {
const { id } = await params;
const job = await getJob(id); // wrapped in React cache()
return {
title: `${job.title} in ${job.city}`,
description: job.summary.slice(0, 155),
alternates: { canonical: `/jobs/${id}` },
};
}38. How do you show remote images, such as company logos, with next/image without hurting performance?
Allow the image host in next.config with images.remotePatterns, as narrowly as you can by protocol, hostname and path; the older domains option is deprecated. When you do not know the size, use the fill prop inside a parent that has position relative and a fixed size, and set sizes so the browser downloads a suitable width; without sizes, a fill image is treated as being as wide as the screen. Use priority only on the one image that is the LCP element. On a self-hosted server optimisation costs CPU, so use a CDN or a custom loader for heavy traffic.
39. Why does a NEXT_PUBLIC_ variable keep its old value after you change it on the server?
NEXT_PUBLIC_ values are written into the JavaScript bundle as plain strings when you run next build. Changing the variable on the server later has no effect on code that is already built. This catches teams that build one Docker image and promote it from staging to production: the browser keeps the staging value. Either build once per environment, or read the value on the server at request time and pass it to Client Components as a prop. Also, only literal references such as process.env.NEXT_PUBLIC_SITE_URL are replaced; a dynamic lookup such as process.env[name] is undefined in the browser.
40. How would you deploy a Next.js app on your own server or in Docker?
Run next build and then next start, which starts a Node.js server that handles rendering, caching, image optimisation and Server Actions. On a virtual machine, keep it running with PM2 or systemd and put nginx in front for TLS, compression and caching of static files. For Docker, set output: "standalone" so the build creates a small folder with server.js and only the node_modules it needs; copy public and .next/static into the image yourself, because they are not included. Set PORT and HOSTNAME, and if nginx buffers responses, turn buffering off for Next.js so streaming still works.
// next.config.js
module.exports = { output: "standalone" };
# Dockerfile, final stage (after npm run build)
WORKDIR /app
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
ENV PORT=3000 HOSTNAME=0.0.0.0
CMD ["node", "server.js"]Ready to check yourself? Take the free Next.js mock test
Next.js interview questions: Hard questions (experienced)
20 questions41. Explain the caching layers of the App Router, and how you would debug stale data.
There are four. Request memoization reuses identical fetch calls within one server render. The Data Cache keeps fetch results on the server across requests and deployments until revalidated. The Full Route Cache keeps the HTML and RSC payload of static routes. The Router Cache keeps visited segments in the browser's memory. To debug, find the layer holding the old data: if a hard refresh fixes it, it is the Router Cache; if not, check whether the build marks the route static, then the fetch options and tags. In dev, logging.fetches in next.config logs each fetch's cache status.
42. In Next.js 15 fetch is not cached by default, so why can a page that uses a plain fetch still be static?
Because caching a fetch and prerendering a route are separate decisions. In Next.js 15 a fetch without cache options is not stored in the Data Cache, but if the route uses no Dynamic API such as cookies, headers or searchParams, Next.js still prerenders it at build time, and the data fetched during next build is baked into the HTML. In development it runs on every request, so the problem appears only in production. For per-request data, pass cache: "no-store", call await connection() from next/server, or set dynamic to force-dynamic; for regular updates, set a revalidate time.
// app/rates/page.tsx (Next.js 15)
export default async function RatesPage() {
// Not cached, but nothing here is request-specific,
// so the route is prerendered once at build time.
const res = await fetch("https://api.example.com/rates");
const data = await res.json();
return <p>1 USD = {data.inr} INR</p>;
}
// For fresh data on every request, use one of:
// fetch(url, { cache: "no-store" })
// await connection(); // from "next/server"
// export const dynamic = "force-dynamic";43. How do you cache database queries that do not use fetch?
Next.js memoizes and caches fetch, but not ORM or SQL calls. React's cache() deduplicates a function within one server request: the layout, page and generateMetadata can all call getCompany(slug) and the query runs once; the result is dropped after the request. To cache across requests, unstable_cache in Next.js 14 and 15 stores the result in the Data Cache, keyed by the arguments and key parts, with revalidate and tags so revalidateTag can clear it. Return plain serialisable data, and never read cookies or headers inside unstable_cache; pass values in as arguments.
import "server-only";
import { cache } from "react";
import { unstable_cache } from "next/cache";
// Once per request, shared by layout, page and metadata
export const getCompany = cache(async (slug: string) =>
db.company.findUnique({ where: { slug } })
);
// Shared across requests for an hour, cleared by a tag
export const getCityJobCount = unstable_cache(
async (city: string) => db.job.count({ where: { city } }),
["city-job-count"],
{ revalidate: 3600, tags: ["jobs"] }
);44. Where should authentication and authorisation checks live in an App Router app?
In layers. Middleware can make a fast optimistic check, such as redirecting when there is no session cookie, but it must not be the only check: it may not cover every path, and a 2025 security advisory showed a crafted header could skip middleware on self-hosted apps until patched. Real checks belong in a data access layer, a server-only module that verifies the session and the record's owner, called from every Server Component, Server Action and Route Handler that touches data. Do not rely on a layout check, as layouts do not re-render on navigation. Return only the fields the UI needs.
// lib/dal.ts
import "server-only";
import { cache } from "react";
import { cookies } from "next/headers";
export const getSession = cache(async () => {
const token = (await cookies()).get("session")?.value;
const session = token ? await verifyToken(token) : null;
return session?.userId ? session : null;
});
export async function getApplication(id: number) {
const session = await getSession();
if (!session) throw new Error("Not signed in");
// the owner check is part of the query itself
return db.application.findFirst({
where: { id, orgId: session.orgId },
select: { id: true, status: true, jobTitle: true },
});
}45. What protections does Next.js give Server Actions, and what must you still do yourself?
Next.js accepts Server Actions only as POST requests and compares the Origin header with the Host header, which blocks most cross-site request forgery; behind a proxy with another host, list it in serverActions.allowedOrigins. Closure values are encrypted, and in Next.js 15 action IDs are unguessable and recalculated between builds, and unused actions are left out of the client bundle. The body is limited to 1 MB by default. None of this checks who the user is: in every action, verify the session, check that the user owns the record, validate input with a schema and rate-limit costly actions.
46. What breaks when you run a Next.js app with ISR on several servers behind a load balancer?
By default each instance keeps the ISR and Data Cache on its own disk and in memory. Servers then return different versions, and revalidateTag on one server does not clear the others. The fix is a shared cache: point cacheHandler in next.config at a handler backed by Redis or similar, and set cacheMaxMemorySize to 0 so instances keep no private copy. If containers are built separately, use generateBuildId so all have the same build ID, and set the same NEXT_SERVER_ACTIONS_ENCRYPTION_KEY everywhere so Server Actions work whichever server answers. Plan for version skew in rolling deploys.
// next.config.js
module.exports = {
cacheHandler: require.resolve("./cache-handler.js"),
cacheMaxMemorySize: 0, // no per-instance memory cache
generateBuildId: async () => process.env.GIT_COMMIT_SHA,
};
// every instance also gets the same
// NEXT_SERVER_ACTIONS_ENCRYPTION_KEY environment variable47. What happens to the HTTP status code when notFound() is called after streaming has started?
Once the first chunk of a streamed response is sent, the headers, including a 200 status, cannot change. If notFound() or redirect() runs later inside a Suspense boundary, Next.js handles it within the stream by rendering the not-found UI or redirecting on the client, and the docs say this does not affect SEO. The status still stays 200, which matters for monitoring and strict crawlers. A loading file puts the page itself inside a boundary. For a real 404 on a missing job, check the record before any boundary, for example with no loading file in or above that segment, and Suspense lower down.
48. What is Partial Prerendering, and would you use it in production on Next.js 15?
Partial Prerendering, or PPR, mixes static and dynamic rendering in one route. At build time Next.js prerenders a static shell with Suspense fallbacks where dynamic parts go. At request time the shell is served at once, from a CDN where possible, and dynamic parts such as a user's saved jobs stream into the same response. Without it, one Dynamic API makes the whole route dynamic. It was a preview in Next.js 14 and is still experimental in Next.js 15, enabled per route by a flag, and the docs advise against production use. Clean Suspense boundaries now make adopting it later easy.
49. How would you build a job details modal that also works as a full page when the link is shared?
Combine a parallel route with an intercepting route. A slot folder @modal is passed to the root layout as a modal prop next to children. Inside it, (.)jobs/[id] intercepts client-side navigation to /jobs/123: the list stays on screen, the job opens in a modal, and the URL changes so it can be shared. On a refresh or an opened link there is no interception, so the normal jobs/[id]/page.tsx renders the full page. Add @modal/default.tsx that returns null, or the slot has nothing to render for other URLs on a full load. Close the modal with router.back().
app/
layout.tsx // renders {children} and {modal}
@modal/
default.tsx // returns null
(.)jobs/[id]/
page.tsx // job inside a modal
jobs/
page.tsx // job list
[id]/page.tsx // full job page
// app/layout.tsx
export default function RootLayout({ children, modal }) {
return (
<html lang="en">
<body>{children}{modal}</body>
</html>
);
}50. How do you reduce the client JavaScript of a large App Router app?
Measure first with @next/bundle-analyzer. Then push "use client" down to small leaf components, because a client file pulls everything it imports into the bundle; a big page marked client is the usual cause. Keep heavy work such as Markdown parsing or date formatting in Server Components, where the libraries cost the browser nothing. Load rare widgets, such as a resume editor, with next/dynamic. Use optimizePackageImports for libraries with large barrel files, import server-only in server modules so a wrong import fails the build, and avoid passing huge objects to Client Components.
"use client";
import dynamic from "next/dynamic";
// Loaded only when this component renders, never on the server
const ResumeEditor = dynamic(() => import("./ResumeEditor"), {
ssr: false,
loading: () => <p>Loading editor...</p>,
});
export function EditResume({ open }: { open: boolean }) {
return open ? <ResumeEditor /> : null;
}51. How can a Server Component start a fetch and let a Client Component use the result without blocking the page?
In Next.js 15 with React 19, the Server Component calls the data function without awaiting it and passes the promise as a prop to a Client Component wrapped in Suspense. The shell streams at once; when the promise resolves on the server, its value streams to the browser, and the Client Component reads it with use(), which suspends until it is ready. An interactive component, such as a sortable review list, gets server data with no client fetch. Do not create the promise inside the Client Component, since each render would fetch again. Errors go to the nearest error boundary.
// app/companies/[slug]/page.tsx (Server Component)
import { Suspense } from "react";
import Reviews from "./Reviews";
export default async function Page({ params }) {
const { slug } = await params;
const reviews = getReviews(slug); // not awaited
return (
<Suspense fallback={<p>Loading reviews...</p>}>
<Reviews reviewsPromise={reviews} />
</Suspense>
);
}
// Reviews.tsx
"use client";
import { use } from "react";
export default function Reviews({ reviewsPromise }) {
const reviews = use(reviewsPromise);
return <ReviewList items={reviews} />;
}52. How do you build optimistic UI with a Server Action?
Use React 19's useOptimistic hook, available in the Next.js 15 App Router. It takes the real state from the server and returns an optimistic copy and a setter. In the form action, set the expected result first, then await the Server Action. The user sees the change at once; when the action finishes and the server data is revalidated, React drops the optimistic value and shows the real state, and if the action fails the UI returns to the old value, so also show an error. Use it for cheap, reversible actions such as saving a job, not for payments or anything that must not look confirmed early.
"use client";
import { useOptimistic } from "react";
import { toggleSavedJob } from "./actions"; // "use server"
export function SaveButton(props: { jobId: number; saved: boolean }) {
const [saved, setSaved] = useOptimistic(
props.saved,
(_current: boolean, next: boolean) => next
);
async function action() {
setSaved(!saved);
await toggleSavedJob(props.jobId); // revalidates the list
}
return (
<form action={action}>
<button>{saved ? "Saved" : "Save job"}</button>
</form>
);
}53. A user saves a job, goes back to the list and sees old data. How do you reason about the client Router Cache?
The Router Cache keeps RSC payloads of visited and prefetched segments in the browser. In Next.js 14, dynamic pages were reused for 30 seconds and prefetched static ones for 5 minutes. In Next.js 15 dynamic pages are not reused by default, but static pages, layouts, loading states and back and forward navigation still come from the cache. If the change came from a Server Action, call revalidatePath or revalidateTag in it, which also clears the Router Cache; setting or deleting a cookie in an action clears it too. For changes made elsewhere, router.refresh() re-renders the route on the server.
54. How would you migrate a large Pages Router app to the App Router?
Move route by route, since both routers can run in one project. Start with the root layout, which replaces _app and _document, then low-risk pages. Map the old APIs: getServerSideProps becomes an async Server Component, getStaticProps with revalidate becomes a revalidate time, getStaticPaths becomes generateStaticParams, next/head becomes the Metadata API, and next/router becomes the next/navigation hooks. API routes can stay in pages/api. A URL must not exist in both folders, and moving between routers is a full page load. Wrap client-only libraries in "use client" files.
55. Design the rendering strategy for the job portal of a Bengaluru startup with 2 lakh job pages.
Split pages by how personal and fresh they are. Home and category pages are static with ISR: revalidate every few minutes plus revalidateTag when an employer posts. Job detail pages use generateStaticParams for the few thousand most visited, render the rest on demand with dynamicParams, clear a per-job tag on edit, and call notFound() for removed jobs. Search depends on searchParams, so it is dynamic, streaming results in Suspense. The candidate dashboard is dynamic and must never go into a shared cache. Add generateMetadata per job, split sitemaps with generateSitemaps, and track LCP.
56. How do you design error handling and error reporting in a production Next.js app?
Separate expected and unexpected errors. Expected ones, like failed validation or a missing record, are returned as values or handled with notFound(). Unexpected ones reach error boundaries: nested error.tsx files stop one failing widget from blanking the page, and global-error.tsx covers the root layout and must render its own html and body. In production, errors from Server Components reach the browser with a generic message and a digest, so no details leak; log the digest server-side to match reports. In Next.js 15, onRequestError in instrumentation.ts reports server errors to monitoring.
// instrumentation.ts (Next.js 15)
import type { Instrumentation } from "next";
export const onRequestError: Instrumentation.onRequestError =
async (err, request, context) => {
await fetch("https://logs.example.in/next-errors", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: (err as Error).message,
path: request.path,
routeType: context.routeType,
}),
});
};57. When would you choose the Edge runtime over the Node.js runtime for a route?
Node.js is the default and the right choice for most pages and APIs: it supports the full npm ecosystem, the file system, native database drivers and ISR. The Edge runtime, set with export const runtime = "edge", offers a smaller set of Web APIs with fast start-up, which suits light work close to the user, such as a geo redirect or a small personalised response. Limits: no fs, many Node packages and TCP database drivers do not work, hosts cap the code size, and if the database is in Mumbai, rendering at a distant edge location adds round trips. Middleware uses the Edge runtime by default.
58. How do you improve the Core Web Vitals of a Next.js site?
Work per metric and use field data, not only Lighthouse. For LCP, serve pages static or with ISR for a fast first byte, stream slow sections, and give the hero image priority and correct sizes. For CLS, use next/image dimensions, next/font fallback metrics, and skeletons the same size as the real content. For INP, ship less client JavaScript by keeping Client Components small, load third-party scripts with next/script using afterInteractive or lazyOnload, and mark heavy updates as transitions. Collect real-user numbers with useReportWebVitals from next/web-vitals and watch the 75th percentile.
59. A page works in next dev but shows stale data or breaks after deployment. What do you check?
First reproduce it with next build and next start locally. In dev every route renders on demand, so static prerendering, the Full Route Cache and build-time fetching never happen; a page that looked fresh can be frozen at build time in production. Read the build output to see which routes are static. Prefetching runs only in production, and Strict Mode runs effects twice only in development. Check which .env files each environment loads and whether NEXT_PUBLIC_ values were baked in at build time. In production, errors show only a digest, so search the server logs for it.
60. When is static export with output: "export" a good choice, and what do you lose?
With it, next build writes plain HTML, CSS and JavaScript to an out folder that any static host or nginx can serve without a Node.js server. It suits marketing sites, documentation and campus event pages. You keep Server Components (run at build time), Client Components and GET Route Handlers that output static files. You lose everything that needs a server per request: cookies, headers, Server Actions, middleware, ISR, redirects and rewrites in next.config, and the default image optimiser. Dynamic routes need generateStaticParams. Next.js 14 removed the next export command.
Practice questions for November 2026
New every month3 questions from the Next.js mock test, the way they appear in it. A new set comes on the 1st of every month. Pick your answer, then open it.
-
How does the Link component make navigation faster in production?
- It compresses images on click
- It turns off JavaScript on the next page
- It caches the whole site in the browser when the app is first installed
- It prefetches the linked route when the link enters the viewport
Show the answer
Answer: It prefetches the linked route when the link enters the viewport. In production, Link prefetches the linked route in the background as it scrolls into view, so a click can show it almost at once. Prefetching is off in development, so speed there is not a fair test.
-
Priya wants a page view logged in useEffect every time the user moves between dashboard pages, but an effect in the dashboard layout runs only once. What should she use?
- A loading.tsx file
- An effect placed inside next.config.js, which runs on each navigation
- A key={Math.random()} on the root html tag
- A template.tsx that renders a Client Component running the effect
Show the answer
Answer: A template.tsx that renders a Client Component running the effect. A layout stays mounted across navigations, so its effects run once. A template gets a fresh instance on each navigation, so an effect inside it runs on every page change. Random keys on html would remount the whole app.
-
A 'use client' Tabs component holds the active tab in state and receives a server-rendered ProductList as children. When the user switches tabs, what happens to ProductList's server code?
- It becomes a Client Component after the first click, so its code then runs in the browser
- It runs again in the browser
- It refetches its data on every tab change
- It does not run again; React reuses the element it received until a new server request
Show the answer
Answer: It does not run again; React reuses the element it received until a new server request. Children from a Server Component arrive as a rendered result in the RSC payload. Client state changes re-render Tabs only; ProductList's server code runs again only on a new server request for the route.
Ready to apply?
There are 119 open jobs asking for Next.js on CoreHRx right now. Create a free profile once and apply with it.