Next.js interviews cover App Router architecture, React Server Components, data fetching strategies, performance optimisation, and deployment. This guide covers 50 of the most common questions — with concise answers and code examples.
Quick reference
| Topic | Key areas |
|---|---|
| App Router | RSC vs Client Components, layouts, loading, error |
| Data Fetching | fetch, caching, revalidation, streaming |
| Rendering | SSR, SSG, ISR, PPR |
| Performance | Image, Font, Script optimisation, bundle splitting |
| API Routes | Route Handlers, Server Actions, middleware |
Architecture & App Router
1. What is the difference between the App Router and the Pages Router?
The Pages Router (pre–Next.js 13) uses pages/ directory, React class/function components, and getServerSideProps/getStaticProps for data fetching.
The App Router (Next.js 13+, stable in 14) uses app/ directory, React Server Components by default, nested layouts, and async/await directly in components.
pages/ → app/
pages/index.tsx → app/page.tsx
pages/_app.tsx → app/layout.tsx
pages/api/foo.ts → app/api/foo/route.ts
2. What are React Server Components (RSC) and how does Next.js use them?
RSC are components that render on the server only — they never ship their JS to the client. In app/ directory, every component is a Server Component by default.
Benefits:
- Zero client-side JS for the component
- Direct database/file-system access
- No need for
useEffect/useStatefor initial data
// app/users/page.tsx — Server Component (default)
async function UsersPage() {
const users = await db.query('SELECT * FROM users'); // direct DB access
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
3. How do you create a Client Component?
Add "use client" directive at the top of the file. Client Components can use hooks, browser APIs, and event handlers.
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Rule: Keep Client Components as leaf nodes. Pass server-fetched data as props from Server Components.
4. What is a layout in the App Router?
A layout.tsx file wraps all pages in its directory and preserves state across navigations (no remount).
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex">
<Sidebar />
<main>{children}</main>
</div>
);
}
Layouts nest — app/layout.tsx (root) → app/dashboard/layout.tsx → app/dashboard/settings/page.tsx.
5. What is the difference between layout.tsx and template.tsx?
| Feature | layout.tsx |
template.tsx |
|---|---|---|
| Remounts on navigation? | No (preserves state) | Yes |
| Use case | Persistent UI (nav, sidebar) | Fresh mount needed (animations, useEffect per page) |
6. What is the purpose of loading.tsx?
loading.tsx creates a Suspense boundary around the page segment. It renders instantly while the page/data loads.
// app/dashboard/loading.tsx
export default function Loading() {
return <Skeleton />;
}
Next.js wraps the page.tsx in <Suspense fallback={<Loading />}> automatically.
7. What is error.tsx and how does it work?
error.tsx is an Error Boundary for a route segment. It must be a Client Component.
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
8. What are parallel routes and how are they useful?
Parallel routes allow multiple pages to render simultaneously in the same layout using @folder slots.
app/
@team/
page.tsx
@analytics/
page.tsx
layout.tsx
// app/layout.tsx
export default function Layout({ team, analytics }: {
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return <>{team}{analytics}</>;
}
Use case: dashboard with independent sections, modals with intercepted routes.
9. What are intercepting routes?
Intercepting routes let you load a route from one part of the app into another — e.g., open a photo in a modal while the URL changes.
Convention: (.), (..), (..)(..), (...) prefixes mirror file-system relative navigation.
app/
photos/
[id]/
page.tsx ← full page at /photos/123
@modal/
(.)photos/
[id]/
page.tsx ← intercepted: shows modal
Data Fetching
10. How do you fetch data in a Server Component?
Use async/await directly — no useEffect needed.
async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 60 }, // ISR: revalidate every 60s
}).then(r => r.json());
return <h1>{product.name}</h1>;
}
11. What are the caching options for fetch in Next.js?
// Default: cached (equivalent to SSG)
fetch(url);
fetch(url, { cache: 'force-cache' });
// No cache (SSR every request)
fetch(url, { cache: 'no-store' });
// ISR: revalidate after N seconds
fetch(url, { next: { revalidate: 60 } });
// Tag-based revalidation
fetch(url, { next: { tags: ['products'] } });
12. What is revalidatePath and revalidateTag?
On-demand revalidation functions called from Server Actions or Route Handlers.
import { revalidatePath, revalidateTag } from 'next/cache';
// After a mutation:
revalidatePath('/products'); // revalidate specific path
revalidateTag('products'); // revalidate all fetches tagged 'products'
13. What is the difference between SSG, SSR, and ISR in Next.js?
| Strategy | How | When data fetched |
|---|---|---|
| SSG | fetch with force-cache (default) |
Build time |
| SSR | fetch with cache: 'no-store' |
Every request |
| ISR | fetch with next: { revalidate: N } |
Build + background refresh |
| PPR | Partial Prerendering (Next.js 14+) | Static shell + dynamic streaming |
14. What is Partial Prerendering (PPR)?
PPR combines static and dynamic rendering in a single route:
- Static HTML shell is served instantly from CDN
- Dynamic parts stream in via Suspense
// next.config.js
const nextConfig = { experimental: { ppr: true } };
// page.tsx
export default async function Page() {
return (
<>
<StaticHeader /> {/* served immediately */}
<Suspense fallback={<Skeleton />}>
<DynamicFeed /> {/* streams in */}
</Suspense>
</>
);
}
15. How do you stream data with loading.tsx and Suspense?
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { RevenueCard, LatestInvoices } from '@/components';
export default function Dashboard() {
return (
<>
<h1>Dashboard</h1>
<Suspense fallback={<RevenueSkeleton />}>
<RevenueCard /> {/* fetches its own data */}
</Suspense>
<Suspense fallback={<InvoiceSkeleton />}>
<LatestInvoices />
</Suspense>
</>
);
}
Each Suspense boundary resolves independently — no waterfall.
Server Actions
16. What are Server Actions?
Server Actions are async functions that run on the server, called directly from Client or Server Components. They replace API routes for mutations.
// app/actions.ts
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function createTodo(formData: FormData) {
const title = formData.get('title') as string;
await db.todo.create({ data: { title } });
revalidatePath('/todos');
}
// app/todos/page.tsx (Server Component)
import { createTodo } from '../actions';
export default function TodoPage() {
return (
<form action={createTodo}>
<input name="title" />
<button type="submit">Add</button>
</form>
);
}
17. How do you call a Server Action from a Client Component?
'use client';
import { useTransition } from 'react';
import { createTodo } from '../actions';
export function AddTodoForm() {
const [isPending, startTransition] = useTransition();
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
startTransition(() => createTodo(formData));
}
return (
<form onSubmit={handleSubmit}>
<input name="title" disabled={isPending} />
<button disabled={isPending}>Add</button>
</form>
);
}
18. How do you handle validation and errors in Server Actions?
'use server';
import { z } from 'zod';
const schema = z.object({ title: z.string().min(1).max(200) });
export async function createTodo(prevState: any, formData: FormData) {
const parsed = schema.safeParse({ title: formData.get('title') });
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors };
}
await db.todo.create({ data: parsed.data });
revalidatePath('/todos');
return { success: true };
}
Use with useActionState (React 19 / Next.js 14.3+) for progressive enhancement.
Routing
19. What are dynamic route segments?
Folder names in brackets create dynamic segments:
app/blog/[slug]/page.tsx → /blog/hello-world (params.slug = "hello-world")
app/shop/[...slugs]/page.tsx → /shop/a/b/c (params.slugs = ["a","b","c"])
app/[[...slugs]]/page.tsx → / or /a/b (optional catch-all)
20. What does generateStaticParams do?
Pre-generates dynamic routes at build time (SSG equivalent for App Router).
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map((post: { slug: string }) => ({ slug: post.slug }));
}
21. How does middleware work in Next.js?
middleware.ts at the project root runs before every matched request — ideal for auth, redirects, A/B testing.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('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*'],
};
22. What is the difference between redirect() and NextResponse.redirect()?
redirect() |
NextResponse.redirect() |
|
|---|---|---|
| Where | Server Components, Server Actions, Route Handlers | Middleware only |
| Throws | Yes (throws NEXT_REDIRECT) | No |
| Status | 307 (temporary) by default | Configurable |
23. How do Route Handlers work?
Route Handlers replace API routes (pages/api/) in the App Router.
// app/api/users/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const users = await db.user.findMany();
return NextResponse.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}
Performance
24. How does next/image optimise images?
<Image> from next/image:
- Converts to WebP/AVIF automatically
- Serves correct size per device (responsive srcset)
- Lazy loads by default (
loading="lazy") - Prevents CLS by requiring
width/heightorfill
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // LCP image — disables lazy loading
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
25. How does next/font work?
next/font downloads fonts at build time, self-hosts them, and eliminates layout shift (no external network request at runtime).
// app/layout.tsx
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}
26. How does next/script work?
Controls when third-party scripts load:
import Script from 'next/script';
// beforeInteractive — critical scripts (blocks page)
// afterInteractive — default, loads after hydration
// lazyOnload — loads during browser idle
// worker — loads in Web Worker (Partytown)
<Script src="https://analytics.io/script.js" strategy="lazyOnload" />
27. What is code splitting in Next.js?
Next.js automatically splits bundles by route. You can also split manually:
import dynamic from 'next/dynamic';
// Component-level code splitting
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <p>Loading chart...</p>,
ssr: false, // disable SSR for browser-only components
});
28. What is the unstable_cache function?
Caches the result of any async function (not just fetch), useful for database queries.
import { unstable_cache } from 'next/cache';
const getCachedUser = unstable_cache(
async (id: string) => db.user.findUnique({ where: { id } }),
['user'],
{ revalidate: 3600, tags: ['user'] }
);
Metadata & SEO
29. How do you define metadata in Next.js App Router?
// Static metadata
export const metadata = {
title: 'My App',
description: 'Description here',
openGraph: {
title: 'My App',
images: ['/og.png'],
},
};
// Dynamic metadata
export async function generateMetadata({ params }: { params: { id: string } }) {
const product = await fetchProduct(params.id);
return {
title: product.name,
description: product.description,
};
}
30. How does the title template work?
// app/layout.tsx
export const metadata = {
title: {
template: '%s | My App',
default: 'My App',
},
};
// app/about/page.tsx
export const metadata = { title: 'About' };
// Result: "About | My App"
Authentication
31. How do you implement authentication in Next.js?
Common approaches:
- NextAuth.js / Auth.js — OAuth, credentials, JWT/sessions
- Clerk / Supabase Auth — managed auth
- Custom JWT in middleware — manual token validation
// middleware.ts with NextAuth
export { auth as middleware } from '@/auth';
export const config = { matcher: ['/dashboard/:path*'] };
32. How do you protect a Server Component?
// app/dashboard/page.tsx
import { auth } from '@/auth';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const session = await auth();
if (!session) redirect('/login');
return <Dashboard user={session.user} />;
}
Deployment & Configuration
33. What output modes does Next.js support?
| Mode | Config | Description |
|---|---|---|
| Node.js server | default | Full Next.js server — required for ISR, Server Actions |
| Standalone | output: 'standalone' |
Minimal Docker image (includes only needed files) |
| Static export | output: 'export' |
Pure HTML/CSS/JS — no server needed (no SSR/ISR) |
34. How do you set up environment variables in Next.js?
.env.local — local dev, not committed
.env.development — dev only
.env.production — production only
.env — all environments (committed)
Exposing to the browser: prefix with NEXT_PUBLIC_.
process.env.SECRET_KEY // server only
process.env.NEXT_PUBLIC_API // available on client
35. What is next.config.js used for?
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [{ hostname: 'cdn.example.com' }],
},
redirects: async () => [
{ source: '/old', destination: '/new', permanent: true },
],
headers: async () => [
{
source: '/(.*)',
headers: [{ key: 'X-Frame-Options', value: 'DENY' }],
},
],
experimental: { ppr: true },
};
module.exports = nextConfig;
Testing
36. How do you test Next.js components?
// __tests__/page.test.tsx (Vitest or Jest)
import { render, screen } from '@testing-library/react';
import HomePage from '@/app/page';
test('renders heading', () => {
render(<HomePage />);
expect(screen.getByRole('heading')).toBeInTheDocument();
});
For Server Components, use Playwright or test utilities from next/experimental/testing.
37. How do you test Route Handlers?
import { GET } from '@/app/api/users/route';
import { NextRequest } from 'next/server';
test('returns users', async () => {
const request = new NextRequest('http://localhost/api/users');
const response = await GET(request);
expect(response.status).toBe(200);
const body = await response.json();
expect(Array.isArray(body)).toBe(true);
});
Advanced Topics
38. What is the difference between cookies() and headers() in Server Components?
import { cookies, headers } from 'next/headers';
// Read cookies (opts into dynamic rendering)
const cookieStore = cookies();
const token = cookieStore.get('token')?.value;
// Read request headers
const headersList = headers();
const userAgent = headersList.get('user-agent');
Both opt the route into dynamic rendering (no static caching).
39. How do useRouter, usePathname, and useSearchParams work?
All require "use client":
'use client';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
export function NavControls() {
const router = useRouter();
const pathname = usePathname(); // "/dashboard/settings"
const searchParams = useSearchParams(); // URLSearchParams
function navigate() {
router.push('/about');
router.replace('/about');
router.back();
router.refresh(); // re-fetch Server Components for current route
}
}
40. What is notFound() and how is it used?
Renders the nearest not-found.tsx file.
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation';
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({ where: { id: params.id } });
if (!product) notFound();
return <ProductView product={product} />;
}
Create app/not-found.tsx for custom 404 UI.
41. What is Suspense streaming and how does it improve TTFB?
Without streaming: server waits for all data → sends full HTML → large TTFB.
With streaming:
- Static shell is sent immediately (low TTFB)
- Async components stream in as HTML chunks
- Browser progressively renders
// Data fetches in parallel, not sequentially:
export default function Page() {
return (
<>
<Suspense fallback={<S1 />}><Component1 /></Suspense>
<Suspense fallback={<S2 />}><Component2 /></Suspense>
</>
);
}
42. How do you share data between Server Components without prop drilling?
Use React.cache() for request-scoped memoization:
// lib/data.ts
import { cache } from 'react';
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
Multiple Server Components calling getUser(id) in one request only hit the DB once.
43. What is the "use server" directive?
Marks a file or function as a Server Action boundary. All exports become callable from Client Components but execute only on the server.
// Entire file is server-only:
'use server';
export async function deleteUser(id: string) { ... }
export async function updateUser(id: string, data: any) { ... }
// Or inline in a component:
export default function Page() {
async function handleDelete(formData: FormData) {
'use server';
await deleteUserFromDB(formData.get('id') as string);
}
return <form action={handleDelete}>...</form>;
}
44. How does Next.js handle fetch deduplication?
In the same request, identical fetch calls are automatically deduplicated (React memoises them). You can also use React.cache for non-fetch async functions.
45. What is opengraph-image.tsx (or .png)?
Next.js generates Open Graph images automatically if you place opengraph-image.tsx in a route segment using the @vercel/og ImageResponse:
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
export const runtime = 'edge';
export const size = { width: 1200, height: 630 };
export default async function OGImage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return new ImageResponse(
<div style={{ fontSize: 60, background: 'white', width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{post.title}
</div>
);
}
46. What is the Edge Runtime and when would you use it?
Edge Runtime runs on CDN edge nodes (Vercel Edge Network, Cloudflare Workers). Faster cold starts than Node.js, but limited API surface (no fs, no native modules).
export const runtime = 'edge'; // or 'nodejs' (default)
Use for: middleware, low-latency API responses, geolocation logic.
47. What are the limitations of Static Export (output: 'export')?
Cannot use:
- Server Actions
- Route Handlers with dynamic logic
next/headers,next/cookiesrevalidateISR- Middleware
- Image Optimization (needs external loader)
Allowed: static HTML, CSS, JS — good for CDN hosting.
48. How does Next.js handle CSS?
| Approach | Import |
|---|---|
| Global CSS | import './globals.css' in root layout |
| CSS Modules | import styles from './component.module.css' |
| Tailwind CSS | PostCSS plugin, works out-of-the-box |
| CSS-in-JS | Client Components only (some libs have RSC support) |
49. How do you implement an API proxy in Next.js?
Option 1 — Route Handler:
// app/api/proxy/[...path]/route.ts
export async function GET(request: Request, { params }: { params: { path: string[] } }) {
const upstream = `https://api.example.com/${params.path.join('/')}`;
const response = await fetch(upstream, { headers: { Authorization: `Bearer ${process.env.API_KEY}` } });
return new Response(response.body, { headers: response.headers });
}
Option 2 — next.config.js rewrites (simpler, no custom code):
async rewrites() {
return [{ source: '/api/:path*', destination: 'https://api.example.com/:path*' }];
}
50. What are common Next.js performance anti-patterns?
| Anti-pattern | Problem | Fix |
|---|---|---|
fetch inside a Client Component |
Waterfall + no caching | Move fetch to Server Component |
Large third-party scripts in <head> |
Blocks rendering | Use next/script with lazyOnload |
Unoptimized images with <img> |
No WebP, no lazy load, CLS | Use next/image |
Sequential await in one component |
Slow TTFB | Use Promise.all or split into Suspense boundaries |
Skipping loading.tsx |
Bad perceived performance | Add Suspense boundaries |
Not using React.cache |
Repeated DB queries | Wrap data fetchers in cache() |
| Importing server libs in Client Components | Bundle bloat / error | Move to Server Components or "use server" |
Missing generateStaticParams for known paths |
SSR instead of SSG | Add generateStaticParams |
Common mistakes
| Mistake | Why it fails | Fix |
|---|---|---|
Using useState in a Server Component |
RSC has no client state | Add "use client" or lift state to client |
| Importing a Server-only module in Client Component | Client bundle error | Use server-only package or move to RSC |
Forgetting await on cookies() / headers() (Next.js 15) |
Returns a Promise in Next.js 15 | const c = await cookies() |
Mutating params or searchParams directly |
Read-only | Destructure only |
Using router.push without "use client" |
Hooks not allowed in RSC | Add "use client" |
Putting secrets in NEXT_PUBLIC_ |
Exposed to browser | Use unprefixed env vars for secrets |
Disabling Image width/height without fill |
CLS and error | Provide dimensions or use fill prop |
Next.js vs alternatives
| Feature | Next.js | Remix | Astro | Nuxt (Vue) |
|---|---|---|---|---|
| RSC support | Yes | Partial | Yes | No (Vue) |
| Server Actions | Yes | Form actions | Yes | Yes |
| File-based routing | Yes | Yes | Yes | Yes |
| ISR | Yes | No (SWR) | No | Yes |
| Edge Runtime | Yes | Yes | Yes | Yes |
| Best for | Full-stack React | Form-heavy apps | Content sites | Vue apps |
FAQ
Q: When should I use Server Actions vs Route Handlers? Server Actions for mutations triggered from UI (forms, buttons). Route Handlers for external webhooks, REST APIs consumed by mobile apps, or endpoints requiring custom headers/status codes.
Q: Can I use Next.js with a separate backend?
Yes. Use fetch in Server Components to call your API, or configure rewrites in next.config.js to proxy requests. Next.js is not required to be your backend.
Q: What is the difference between app/ and pages/ in the same project?
Both can coexist in Next.js 13/14. Pages Router takes precedence for conflicting routes. Migrate incrementally — but app/ is the future.
Q: Why does my Server Component still cause client-side JS?
If you import a Client Component, its JS is included. Make sure RSC-only imports are not used in "use client" files. Use server-only package to guard server code.
Q: What does router.refresh() do?
Re-fetches Server Components for the current route without a full page reload. Useful after a mutation to update server-rendered data.
Q: Is Next.js 15 backwards compatible with Next.js 14?
Mostly. Breaking changes include cookies(), headers(), params, and searchParams becoming Promise-based (async). Codemods are available: npx @next/codemod@latest upgrade.