InterviewHack.ai
Start free
Blog/Next.js Interview Questions and How to Answer Them (40+ Questions)

Next.js Interview Questions and How to Answer Them (40+ Questions)

September 16, 2026

nextjsreact

Complete Next.js interview preparation article with 45 numbered questions, detailed answers, and production-quality code examples covering foundations, data fetching, routing, performance, API routes, advanced patterns, testing, and deployment.

Next.js Interview Questions and How to Answer Them (40+ Questions)

Next.js is the dominant React framework for production applications. If you have a frontend or full-stack interview coming up, this is the resource you need — 40+ real questions, detailed answers, and working code that mirrors what interviewers actually expect.

This guide is organized from foundational concepts through advanced topics so you can read start-to-finish or jump directly to the area you need to strengthen.


Foundational Concepts

1. What is Next.js and why would you use it over plain React?

Next.js is a React framework that adds server-side rendering (SSR), static site generation (SSG), file-based routing, API routes, image optimization, and built-in TypeScript support on top of React.

The core reasons to reach for Next.js over a plain React SPA:

  • SEO — search engines can read server-rendered HTML immediately; a blank
    cannot be indexed until JavaScript executes
  • Performance — pages can ship zero JavaScript for static content; React Server Components (RSC) remove client-side data-fetching waterfalls
  • Routing — file-system routing eliminates manual react-router configuration
  • Full-stack — API routes and Server Actions mean you can write backend logic in the same codebase without a separate Express server
  • Image optimization — automatic WebP conversion, lazy loading, and layout shift prevention via next/image

Answer pattern for this question: Start with "it's a production framework built on React," name the rendering models (SSR, SSG, ISR, RSC), then name a concrete trade-off you've made in real work (e.g., "we chose Next.js for our marketing site because organic search was a growth channel and a CSR SPA would have hurt rankings").


2. Explain the difference between Pages Router and App Router

Next.js has two routing paradigms that coexist in the same project.

Pages Router (stable since v1, still fully supported):

  • Files in /pages map to routes
  • Data fetching via getServerSideProps, getStaticProps, getStaticPaths
  • Components are traditional React client components
  • API routes in /pages/api

App Router (stable since v13.4):

  • Files in /app map to routes using page.tsx convention
  • Components are React Server Components by default — they run on the server, never ship to the browser
  • Data fetching is plain async/await inside components or fetch() with extended caching semantics
  • Layouts via layout.tsx, loading states via loading.tsx, error boundaries via error.tsx
  • Server Actions for mutations
tsx
// App Router — Server Component (no "use client" directive)
// This component runs only on the server. Zero JS shipped for this component.
async function UserProfile({ id }: { id: string }) {
  // Direct database/API call — no useEffect, no loading state on client
  const user = await db.users.findUnique({ where: { id } });

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}
tsx
// Pages Router equivalent
// getServerSideProps runs on the server, result passed as props
export async function getServerSideProps({ params }) {
  const user = await db.users.findUnique({ where: { id: params.id } });
  return { props: { user } };
}

export default function UserProfile({ user }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

Interview tip: Interviewers ask this to see whether you understand RSC. The key insight is that in the App Router, the default is server — you opt into the client with "use client", not the other way around. This inverts the mental model from Pages Router.


3. What are React Server Components (RSC) and how do they work in Next.js?

React Server Components are components that execute exclusively on the server. They can access databases, file systems, and secrets directly. They never hydrate on the client — meaning no JavaScript bundle for those components reaches the browser.

Key properties:

  • Cannot use hooks (useState, useEffect, useContext)
  • Cannot use browser APIs
  • Cannot attach event listeners
  • Can be async — await database calls directly
  • Can import server-only modules (e.g., bcrypt, fs) without bundle bloat
tsx
// app/dashboard/page.tsx — Server Component
import { db } from "@/lib/db";
import { cookies } from "next/headers";

// This runs on the server. The DB connection string never reaches the client.
export default async function DashboardPage() {
  const session = cookies().get("session")?.value;
  const user = await db.user.findUnique({
    where: { sessionToken: session },
    include: { applications: { take: 10, orderBy: { createdAt: "desc" } } },
  });

  if (!user) redirect("/login");

  return (
    <main>
      <h1>Welcome, {user.name}</h1>
      {/* Pass serializable props down to a Client Component for interactivity */}
      <ApplicationList initialData={user.applications} />
    </main>
  );
}
tsx
// components/ApplicationList.tsx — Client Component
"use client";
import { useState } from "react";

export function ApplicationList({ initialData }) {
  // useState is fine here — this is a Client Component
  const [applications, setApplications] = useState(initialData);

  return (
    <ul>
      {applications.map((app) => (
        <li key={app.id}>{app.company}</li>
      ))}
    </ul>
  );
}

The component tree rule: Client Components can import other Client Components. Server Components can import both. But a Client Component cannot import a Server Component directly — you can only pass Server Components as children props into Client Components.

tsx
// ✅ Valid: Server Component wraps Client Component, passing RSC as children
// app/layout.tsx
import { Modal } from "@/components/Modal"; // Client Component
import { Sidebar } from "@/components/Sidebar"; // Server Component

export default function Layout({ children }) {
  return (
    <Modal>
      {/* Sidebar is a Server Component passed as children to a Client Component */}
      <Sidebar />
      {children}
    </Modal>
  );
}

4. What is the difference between SSR, SSG, ISR, and CSR in Next.js?

This is one of the most common Next.js interview questions. Interviewers want to see you understand the rendering spectrum and know when to apply each.

| Rendering | When HTML is generated | Data freshness | Use case |

|-----------|----------------------|----------------|----------|

| SSG (Static Site Generation) | Build time | Stale until next build | Blog posts, marketing pages, docs |

| ISR (Incremental Static Regeneration) | Build time + background regeneration | Up to N seconds stale | Product pages, news, dashboards with tolerable lag |

| SSR (Server-Side Rendering) | Each request | Always fresh | Personalized pages, auth-gated content, real-time data |

| CSR (Client-Side Rendering) | In the browser after hydration | Fetched client-side | User dashboards, search results that don't need SEO |

Pages Router implementation:

tsx
// SSG — runs once at build time
export async function getStaticProps() {
  const posts = await fetchBlogPosts();
  return {
    props: { posts },
    revalidate: 60, // ISR: regenerate if request comes after 60 seconds
  };
}

// SSR — runs on every request
export async function getServerSideProps(context) {
  const { req } = context;
  const session = getSession(req);
  const data = await fetchPersonalizedData(session.userId);
  return { props: { data } };
}

App Router implementation:

tsx
// SSG — no dynamic data, Next.js statically generates at build time
export default async function BlogPost({ params }) {
  const post = await getPost(params.slug); // cached by default
  return <Article post={post} />;
}

// ISR — revalidate after 60 seconds
async function getData() {
  const res = await fetch("https://api.example.com/products", {
    next: { revalidate: 60 },
  });
  return res.json();
}

// SSR — no caching, fresh on every request
async function getData() {
  const res = await fetch("https://api.example.com/user", {
    cache: "no-store",
  });
  return res.json();
}

Interview answer pattern: "It depends on the data's freshness requirements and SEO needs. Static with ISR is my default for anything that can tolerate a short lag. SSR when the page must be personalized per request. CSR when SEO doesn't matter and data changes per user interaction."


5. How does file-based routing work in the App Router?

The App Router uses a folder-based routing system where each folder in app/ represents a route segment. The actual UI is in special files within those folders.

Reserved filenames:

| File | Purpose |

|------|---------|

| page.tsx | The UI for that route — makes the segment publicly accessible |

| layout.tsx | Shared UI that wraps child routes, persists across navigation |

| loading.tsx | Automatic loading skeleton using React Suspense |

| error.tsx | Error boundary for the segment and its children |

| not-found.tsx | Custom 404 page |

| route.ts | API endpoint (replaces pages/api) |

| template.tsx | Like layout but remounts on navigation (for animations) |

| middleware.ts | Runs before every request (root-level only) |

app/
├── layout.tsx          → root layout (wraps everything)
├── page.tsx            → /
├── blog/
│   ├── layout.tsx      → wraps /blog and /blog/[slug]
│   ├── page.tsx        → /blog
│   └── [slug]/
│       ├── page.tsx    → /blog/some-post
│       └── loading.tsx → automatic Suspense while fetching
├── (marketing)/        → route group — no URL segment
│   ├── about/page.tsx  → /about
│   └── pricing/page.tsx → /pricing
└── dashboard/
    ├── @sidebar/       → parallel route — rendered simultaneously
    │   └── page.tsx
    └── page.tsx        → /dashboard

Dynamic segments:

tsx
// app/jobs/[id]/page.tsx
export default function JobPage({ params }: { params: { id: string } }) {
  return <div>Job ID: {params.id}</div>;
}

// app/blog/[...slug]/page.tsx — catch-all: matches /blog/a/b/c
export default function BlogPage({ params }: { params: { slug: string[] } }) {
  return <div>{params.slug.join("/")}</div>;
}

Data Fetching

6. How does data fetching work in the App Router?

The App Router treats fetch() as a first-class primitive and extends its caching semantics. Server Components are async by default, so you fetch data directly in the component body.

tsx
// Cached indefinitely (default) — equivalent to SSG
async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`);
  return res.json();
}

// No cache — runs on every request (equivalent to SSR)
async function getLivePrice(id: string) {
  const res = await fetch(`https://api.example.com/prices/${id}`, {
    cache: "no-store",
  });
  return res.json();
}

// Time-based revalidation (equivalent to ISR)
async function getPopularPosts() {
  const res = await fetch("https://api.example.com/posts/popular", {
    next: { revalidate: 300 }, // 5 minutes
  });
  return res.json();
}

// Tag-based revalidation
async function getPosts() {
  const res = await fetch("https://api.example.com/posts", {
    next: { tags: ["posts"] },
  });
  return res.json();
}

// On-demand revalidation in a Server Action or Route Handler
import { revalidateTag } from "next/cache";

export async function POST() {
  revalidateTag("posts"); // Invalidate all fetches tagged "posts"
  return Response.json({ revalidated: true });
}

Request deduplication: Next.js automatically deduplicates identical fetch() calls within a single render pass — if 5 components fetch the same URL, only 1 HTTP request is made.

Non-fetch data sources (ORMs, databases, SDKs):

tsx
import { cache } from "react";

// Wrap in React's cache() for deduplication when not using fetch()
export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } });
});

7. What are Server Actions and how do you use them?

Server Actions are async functions that run on the server but can be called directly from Client Components — including form submissions. They replace the need for manual API endpoints for mutations.

tsx
// app/actions.ts
"use server"; // Marks all exports as Server Actions

import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";

export async function createApplication(formData: FormData) {
  const company = formData.get("company") as string;
  const role = formData.get("role") as string;

  // Runs on server — direct DB access, no fetch needed
  await db.application.create({
    data: { company, role, userId: getAuthenticatedUserId() },
  });

  // Revalidate the page so fresh data is shown
  revalidatePath("/dashboard");
}
tsx
// app/apply/page.tsx — using Server Action in a form
import { createApplication } from "@/app/actions";

export default function ApplyPage() {
  return (
    // action prop accepts a Server Action directly
    <form action={createApplication}>
      <input name="company" type="text" required />
      <input name="role" type="text" required />
      <button type="submit">Apply</button>
    </form>
  );
}
tsx
// Client Component calling a Server Action programmatically
"use client";
import { createApplication } from "@/app/actions";
import { useTransition } from "react";

export function ApplyButton({ company, role }) {
  const [isPending, startTransition] = useTransition();

  return (
    <button
      onClick={() => {
        startTransition(async () => {
          await createApplication({ company, role });
        });
      }}
      disabled={isPending}
    >
      {isPending ? "Applying..." : "Apply"}
    </button>
  );
}

Security note: Server Actions are automatically secured — Next.js uses a secret token to ensure only your app can invoke them. Still, always validate input on the server. Don't trust client-supplied data.


8. What is `getStaticPaths` and when do you need it?

getStaticPaths (Pages Router) tells Next.js which dynamic routes to pre-render at build time. You need it whenever you have a dynamic route ([slug]) combined with getStaticProps.

tsx
// pages/blog/[slug].tsx

export async function getStaticPaths() {
  const posts = await fetchAllPosts();

  return {
    // Pre-build these specific paths
    paths: posts.map((post) => ({ params: { slug: post.slug } })),

    // fallback: false → 404 for paths not in the list
    // fallback: true → show loading state, generate on first request
    // fallback: 'blocking' → wait for generation, no loading state
    fallback: "blocking",
  };
}

export async function getStaticProps({ params }) {
  const post = await fetchPost(params.slug);

  if (!post) return { notFound: true };

  return {
    props: { post },
    revalidate: 3600, // ISR: regenerate hourly
  };
}

App Router equivalent:

tsx
// app/blog/[slug]/page.tsx

// generateStaticParams replaces getStaticPaths
export async function generateStaticParams() {
  const posts = await fetchAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function BlogPost({ params }) {
  const post = await fetchPost(params.slug);
  return <Article post={post} />;
}

Routing & Navigation

9. How does navigation work in Next.js and what is the difference between `<Link>`, `useRouter`, and `redirect()`?

tsx
// <Link> — declarative, prefetches on hover/viewport, preferred for UI
import Link from "next/link";

export function Nav() {
  return (
    <nav>
      <Link href="/dashboard">Dashboard</Link>
      {/* Prefetch is true by default in production */}
      <Link href="/blog" prefetch={false}>Blog</Link>
      {/* Replace instead of push to history */}
      <Link href="/onboarding" replace>Start</Link>
    </nav>
  );
}
tsx
// useRouter — programmatic navigation in Client Components
"use client";
import { useRouter } from "next/navigation"; // App Router
// import { useRouter } from "next/router"; // Pages Router

export function SearchForm() {
  const router = useRouter();

  function handleSubmit(e) {
    e.preventDefault();
    const query = new FormData(e.target).get("q");
    router.push(`/search?q=${query}`);
    // router.replace, router.back(), router.refresh() also available
  }

  return <form onSubmit={handleSubmit}>...</form>;
}
tsx
// redirect() — Server-side redirect from Server Components or Server Actions
import { redirect } from "next/navigation";

export default async function ProtectedPage() {
  const session = await getSession();

  if (!session) {
    redirect("/login"); // Throws internally — no return needed after
  }

  return <Dashboard />;
}

Key differences:

  • is for rendered links in JSX — it handles prefetching automatically
  • useRouter is for programmatic navigation after an event (form submit, button click) — Client Components only
  • redirect() is for server-side conditional redirects — runs before the page renders

10. What is Middleware in Next.js and what can you use it for?

Middleware runs before a request is completed — it can rewrite URLs, redirect, modify headers, or short-circuit a request entirely. It runs at the Edge (Vercel's network), making it extremely fast.

ts
// middleware.ts (root of project)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;

  // Auth check — redirect unauthenticated users
  if (pathname.startsWith("/dashboard")) {
    const token = request.cookies.get("session")?.value;
    if (!token) {
      return NextResponse.redirect(new URL("/login", request.url));
    }
  }

  // Geolocation-based routing
  const country = request.geo?.country || "US";
  if (pathname === "/" && country === "AR") {
    return NextResponse.rewrite(new URL("/es", request.url));
  }

  // A/B test via cookie
  if (pathname === "/landing") {
    const bucket = request.cookies.get("ab-bucket")?.value || "a";
    return NextResponse.rewrite(new URL(`/landing-${bucket}`, request.url));
  }

  return NextResponse.next();
}

// Only run middleware on these paths (avoids running on static files)
export const config = {
  matcher: ["/dashboard/:path*", "/", "/landing"],
};

What Middleware can and cannot do:

| Can do | Cannot do |

|--------|-----------|

| Read/set cookies | Access the file system |

| Rewrite/redirect URLs | Use Node.js APIs (fs, crypto, etc.) |

| Modify request/response headers | Make long-running database queries |

| Read geolocation, IP, user agent | Import large Node.js packages |

Middleware runs on the Edge Runtime — a lightweight JS environment. For heavy logic, use API routes or Server Components instead.


11. How do parallel routes and intercepting routes work?

These are advanced App Router features for complex UI patterns.

Parallel Routes — render multiple pages simultaneously in the same layout:

app/dashboard/
├── layout.tsx          → renders @analytics and @team simultaneously
├── page.tsx            → /dashboard main content
├── @analytics/
│   └── page.tsx        → rendered in the "analytics" slot
└── @team/
    └── page.tsx        → rendered in the "team" slot
tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,  // named slot matching @analytics folder
  team,       // named slot matching @team folder
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <div className="grid grid-cols-3">
      <main>{children}</main>
      <aside>{analytics}</aside>
      <aside>{team}</aside>
    </div>
  );
}

Intercepting Routes — intercept a route to show it in the current context (e.g., photo modal without leaving the feed):

app/
├── photos/[id]/page.tsx      → full photo page at /photos/123
└── (.)photos/[id]/page.tsx   → intercepted — shows modal over feed

The (.) prefix intercepts same-level routes. (..) intercepts one level up, (...) intercepts from root.


Performance & Optimization

12. How does `next/image` work and why should you use it?

next/image automatically handles:

  • Format conversion — serves WebP/AVIF to supporting browsers
  • Responsive sizing — generates multiple sizes, browser picks correct one
  • Lazy loading — loads below-fold images only when needed
  • Layout shift prevention — requires width/height or fill to reserve space (CLS = 0)
  • CDN delivery — serves from Next.js's built-in image CDN (or your own)
tsx
import Image from "next/image";

// Fixed size image
<Image
  src="/hero.png"
  alt="Hero image"
  width={800}
  height={600}
  priority // LCP image — skip lazy loading, preload instead
/>

// Fill container (needs position:relative parent)
<div className="relative h-96">
  <Image
    src={post.coverImage}
    alt={post.title}
    fill
    sizes="(max-width: 768px) 100vw, 50vw"
    className="object-cover"
  />
</div>

// Remote image — must whitelist domain in next.config.mjs
// next.config.mjs
export default {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "cdn.example.com",
        pathname: "/uploads/**",
      },
    ],
  },
};

Common mistake: Forgetting priority on the LCP (Largest Contentful Paint) image. The hero image is usually LCP — without priority, it's lazy-loaded, hurting your Core Web Vitals score.


13. What is `next/font` and how does it eliminate layout shift from fonts?

Custom fonts cause layout shift because the browser first renders with a fallback font, then swaps to the loaded font — changing element sizes.

next/font solves this by:

  1. 1Downloading fonts at build time and self-hosting them (zero external requests)
  2. 2Automatically generating CSS size-adjust, ascent-override, and descent-override properties so the fallback font matches the target font's metrics exactly
tsx
// app/layout.tsx
import { Inter, Roboto_Mono } from "next/font/google";
import localFont from "next/font/local";

const inter = Inter({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-inter", // CSS variable for Tailwind
});

const mono = Roboto_Mono({
  subsets: ["latin"],
  weight: ["400", "700"],
  variable: "--font-mono",
});

// Local font
const brand = localFont({
  src: [
    { path: "./fonts/BrandFont-Regular.woff2", weight: "400" },
    { path: "./fonts/BrandFont-Bold.woff2", weight: "700" },
  ],
  variable: "--font-brand",
});

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.variable} ${mono.variable}`}>
      <body className={inter.className}>{children}</body>
    </html>
  );
}
ts
// tailwind.config.ts
export default {
  theme: {
    extend: {
      fontFamily: {
        sans: ["var(--font-inter)"],
        mono: ["var(--font-mono)"],
      },
    },
  },
};

14. How does code splitting work in Next.js?

Next.js automatically splits code at the route level — each page gets its own JavaScript bundle. Only the code for the current page is loaded.

Dynamic imports for component-level splitting:

tsx
import dynamic from "next/dynamic";

// Split off a heavy component (e.g., a rich text editor)
const RichEditor = dynamic(() => import("@/components/RichEditor"), {
  loading: () => <div className="h-40 animate-pulse bg-gray-200 rounded" />,
  ssr: false, // Editor uses browser APIs — skip SSR entirely
});

// Split off a component only needed conditionally
const PricingModal = dynamic(() => import("@/components/PricingModal"));

export function Dashboard({ isPremium }) {
  return (
    <div>
      <RichEditor />
      {!isPremium && <PricingModal />}
    </div>
  );
}

When to use ssr: false:

  • Components that use window, document, localStorage
  • Components that rely on browser-only libraries (e.g., Leaflet maps, Chart.js canvas)
  • Components that differ significantly between server and client (preventing hydration mismatches)

15. How do you optimize Core Web Vitals in a Next.js app?

Core Web Vitals are Google's user-experience metrics: LCP (loading), INP/FID (interactivity), and CLS (visual stability).

LCP (Largest Contentful Paint) — target: under 2.5s

tsx
// 1. Add priority to the LCP image
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority />

// 2. Preload critical fonts (next/font handles this automatically)

// 3. Reduce server response time — use SSG/ISR instead of SSR for public pages

// 4. Use next/image for automatic WebP serving and CDN caching

CLS (Cumulative Layout Shift) — target: under 0.1

tsx
// Reserve space for images — always provide width/height
<Image width={400} height={300} ... />

// Reserve space for ads/embeds with min-height
<div style={{ minHeight: "250px" }}>
  <Advertisement />
</div>

// Use next/font to prevent font swap layout shift

INP (Interaction to Next Paint) — target: under 200ms

tsx
"use client";
import { useTransition } from "react";

export function FilterButton({ onFilter }) {
  const [isPending, startTransition] = useTransition();

  return (
    <button
      onClick={() => {
        // Mark state update as non-urgent — keeps UI responsive
        startTransition(() => onFilter("newest"));
      }}
    >
      {isPending ? "Loading..." : "Sort by Newest"}
    </button>
  );
}

API Routes & Backend

16. How do Route Handlers work in the App Router?

Route Handlers replace pages/api files. They live in app/ and use the Web Request/Response APIs.

ts
// app/api/jobs/route.ts
import { NextRequest, NextResponse } from "next/server";

// GET /api/jobs?q=react
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const query = searchParams.get("q") ?? "";

  const jobs = await db.job.findMany({
    where: { title: { contains: query, mode: "insensitive" } },
    take: 20,
  });

  return NextResponse.json({ jobs });
}

// POST /api/jobs
export async function POST(request: NextRequest) {
  const body = await request.json();

  // Validate input
  const parsed = jobSchema.safeParse(body);
  if (!parsed.success) {
    return NextResponse.json({ error: parsed.error }, { status: 400 });
  }

  const job = await db.job.create({ data: parsed.data });
  return NextResponse.json({ job }, { status: 201 });
}
ts
// app/api/jobs/[id]/route.ts — dynamic route handlers
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const job = await db.job.findUnique({ where: { id: params.id } });
  if (!job) return NextResponse.json({ error: "Not found" }, { status: 404 });
  return NextResponse.json(job);
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  await db.job.delete({ where: { id: params.id } });
  return new Response(null, { status: 204 });
}

Caching Route Handlers:

ts
// Cached (static) — result is cached at build time
export const dynamic = "force-static";

// Never cached
export const dynamic = "force-dynamic";

17. How do you handle authentication in Next.js?

The most common pattern is session-based auth with JWTs or server-side sessions, validated in Middleware.

ts
// lib/auth.ts — using jose for JWT verification at Edge
import { jwtVerify } from "jose";

const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);

export async function verifyToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, JWT_SECRET);
    return payload as { userId: string; email: string };
  } catch {
    return null;
  }
}
ts
// middleware.ts — protect routes at the Edge
import { NextResponse } from "next/server";
import { verifyToken } from "@/lib/auth";

export async function middleware(request) {
  const token = request.cookies.get("session")?.value;
  const payload = token ? await verifyToken(token) : null;

  if (!payload && request.nextUrl.pathname.startsWith("/dashboard")) {
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("next", request.nextUrl.pathname);
    return NextResponse.redirect(loginUrl);
  }

  // Forward user identity to Server Components via headers
  const response = NextResponse.next();
  if (payload) {
    response.headers.set("x-user-id", payload.userId);
  }
  return response;
}
tsx
// Server Component reading the user from headers
import { headers } from "next/headers";

export default async function DashboardPage() {
  const userId = headers().get("x-user-id");
  const user = await db.user.findUnique({ where: { id: userId } });
  return <Dashboard user={user} />;
}

NextAuth.js / Auth.js is the community standard for production auth:

ts
// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import { DrizzleAdapter } from "@auth/drizzle-adapter";

const handler = NextAuth({
  adapter: DrizzleAdapter(db),
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
  ],
  callbacks: {
    session({ session, user }) {
      session.user.id = user.id;
      return session;
    },
  },
});

export { handler as GET, handler as POST };

18. How do you handle environment variables in Next.js?

bash
# .env.local (never committed)
DATABASE_URL=postgresql://...
JWT_SECRET=supersecret

# Prefix with NEXT_PUBLIC_ to expose to the browser
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_APP_URL=https://myapp.com
ts
// Server-only (safe)
const db = new PrismaClient({
  datasources: { db: { url: process.env.DATABASE_URL } },
});

// Client-safe (bundled into JS — never put secrets here)
const stripe = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);

Type-safe environment variables with @t3-oss/env-nextjs:

ts
// env.mjs
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    JWT_SECRET: z.string().min(32),
  },
  client: {
    NEXT_PUBLIC_APP_URL: z.string().url(),
  },
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    JWT_SECRET: process.env.JWT_SECRET,
    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
  },
});

// Build fails immediately if env vars are missing or wrong shape

Advanced Topics

19. What is the difference between `useRouter` from `next/navigation` vs `next/router`?

ts
// next/router — Pages Router ONLY
import { useRouter } from "next/router";

// next/navigation — App Router ONLY
import { useRouter } from "next/navigation";
import { usePathname, useSearchParams } from "next/navigation";

App Router navigation hooks:

tsx
"use client";
import { useRouter, usePathname, useSearchParams } from "next/navigation";

export function SearchComponent() {
  const router = useRouter();
  const pathname = usePathname(); // e.g., "/dashboard/jobs"
  const searchParams = useSearchParams(); // URLSearchParams instance
  const query = searchParams.get("q") ?? "";

  function updateSearch(newQuery: string) {
    const params = new URLSearchParams(searchParams);
    params.set("q", newQuery);
    // push vs. replace — replace avoids polluting browser history for filters
    router.replace(`${pathname}?${params.toString()}`);
  }

  return <input value={query} onChange={(e) => updateSearch(e.target.value)} />;
}

Common mistake: Using useSearchParams in a Server Component. It only works in Client Components. For Server Components, read search params from page.tsx props instead:

tsx
// Server Component — searchParams come as props
export default function SearchPage({
  searchParams,
}: {
  searchParams: { q?: string };
}) {
  const query = searchParams.q ?? "";
  return <Results query={query} />;
}

20. How do you implement Streaming in Next.js?

Streaming sends HTML to the browser in chunks as it becomes ready — users see meaningful content before the entire page is generated.

tsx
// app/dashboard/page.tsx
import { Suspense } from "react";

// This page streams: the shell renders immediately,
// each Suspense boundary streams in as its data resolves
export default function DashboardPage() {
  return (
    <main>
      {/* Renders immediately — no data needed */}
      <DashboardHeader />

      {/* Streams in with its own loading skeleton */}
      <Suspense fallback={<StatsSkeleton />}>
        <Stats /> {/* Async Server Component */}
      </Suspense>

      {/* Streams in independently — doesn't block Stats */}
      <Suspense fallback={<JobListSkeleton />}>
        <RecentJobs /> {/* Another async Server Component */}
      </Suspense>
    </main>
  );
}

// Stats fetches its own data — runs in parallel with RecentJobs
async function Stats() {
  const stats = await getStats(); // 200ms
  return <StatsDisplay stats={stats} />;
}

async function RecentJobs() {
  const jobs = await getRecentJobs(); // 800ms — doesn't block Stats
  return <JobList jobs={jobs} />;
}

loading.tsx — automatic Suspense wrapper:

tsx
// app/dashboard/loading.tsx
// Next.js automatically wraps the page in <Suspense fallback={<Loading />}>
export default function Loading() {
  return <DashboardSkeleton />;
}

21. How do you handle errors in Next.js App Router?

tsx
// app/dashboard/error.tsx — Error boundary for /dashboard and its children
"use client"; // Error components must be Client Components

import { useEffect } from "react";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void; // Retry function provided by Next.js
}) {
  useEffect(() => {
    // Log to error monitoring service
    console.error(error);
    Sentry.captureException(error);
  }, [error]);

  return (
    <div>
      <h2>Something went wrong</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}
tsx
// app/global-error.tsx — catches errors in root layout
"use client";

export default function GlobalError({ error, reset }) {
  return (
    <html>
      <body>
        <h2>Application error</h2>
        <button onClick={reset}>Reload</button>
      </body>
    </html>
  );
}

Not Found pages:

tsx
// app/not-found.tsx
export default function NotFound() {
  return (
    <div>
      <h1>404 — Page not found</h1>
      <Link href="/">Go home</Link>
    </div>
  );
}

// In a Server Component — trigger not-found
import { notFound } from "next/navigation";

async function JobPage({ params }) {
  const job = await getJob(params.id);
  if (!job) notFound(); // Renders not-found.tsx
  return <JobDetail job={job} />;
}

22. How do you implement ISR (Incremental Static Regeneration) with on-demand revalidation?

ts
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from "next/cache";
import { NextRequest } from "next/server";

export async function POST(request: NextRequest) {
  // Validate webhook secret
  const secret = request.headers.get("x-webhook-secret");
  if (secret !== process.env.REVALIDATION_SECRET) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const body = await request.json();

  if (body.type === "post.updated") {
    // Revalidate specific path
    revalidatePath(`/blog/${body.slug}`);
    revalidatePath("/blog"); // Revalidate listing page too
  }

  if (body.type === "products.updated") {
    // Revalidate all pages tagged "products"
    revalidateTag("products");
  }

  return Response.json({ revalidated: true, timestamp: Date.now() });
}
tsx
// Tag your fetches for targeted invalidation
async function getProducts() {
  const res = await fetch("https://api.example.com/products", {
    next: { tags: ["products"] },
  });
  return res.json();
}

// For non-fetch data sources
import { unstable_cache } from "next/cache";

const getCachedProducts = unstable_cache(
  async () => db.product.findMany(),
  ["products-list"], // cache key
  { tags: ["products"], revalidate: 3600 }
);

23. What is the `<Suspense>` boundary and how does it interact with Next.js?

Suspense is a React primitive that Next.js uses for three things: streaming HTML, lazy-loading Client Components, and handling async data fetching with loading states.

tsx
import { Suspense } from "react";
import dynamic from "next/dynamic";

// Dynamic import creates an automatic Suspense point
const HeavyChart = dynamic(() => import("@/components/Chart"), {
  loading: () => <ChartSkeleton />,
  ssr: false,
});

// Nested Suspense — granular loading control
export default function AnalyticsPage() {
  return (
    <div>
      {/* This loads fast — no Suspense needed */}
      <PageHeader title="Analytics" />

      {/* Outer Suspense shows skeleton while inner data loads */}
      <Suspense fallback={<SectionSkeleton />}>
        <MetricsSection>
          {/* Inner Suspense for a slower data dependency */}
          <Suspense fallback={<ChartSkeleton />}>
            <RevenueChart />
          </Suspense>
          {/* This renders independently of RevenueChart */}
          <ConversionRate />
        </MetricsSection>
      </Suspense>
    </div>
  );
}

The use() hook (React 19 / Next.js 15+):

tsx
"use client";
import { use, Suspense } from "react";

// Pass a Promise from Server Component to Client Component
export function ClientComponent({ dataPromise }: { dataPromise: Promise<Data> }) {
  // use() unwraps the promise — triggers Suspense above while pending
  const data = use(dataPromise);
  return <div>{data.title}</div>;
}

// Server Component
export default function Page() {
  const dataPromise = fetchData(); // Don't await — pass the promise
  return (
    <Suspense fallback={<Skeleton />}>
      <ClientComponent dataPromise={dataPromise} />
    </Suspense>
  );
}

24. How do you implement optimistic updates with Server Actions?

tsx
"use client";
import { useOptimistic, useTransition } from "react";
import { toggleLike } from "@/app/actions";

interface Post {
  id: string;
  likes: number;
  likedByUser: boolean;
}

export function LikeButton({ post }: { post: Post }) {
  const [isPending, startTransition] = useTransition();

  // useOptimistic — show predicted UI state before server confirms
  const [optimisticPost, addOptimisticLike] = useOptimistic(
    post,
    (currentPost, liked: boolean) => ({
      ...currentPost,
      likedByUser: liked,
      likes: liked ? currentPost.likes + 1 : currentPost.likes - 1,
    })
  );

  function handleLike() {
    startTransition(async () => {
      // Update UI immediately — don't wait for server
      addOptimisticLike(!optimisticPost.likedByUser);

      // Then sync with server
      await toggleLike(post.id);
      // If toggleLike throws, React automatically reverts optimistic state
    });
  }

  return (
    <button onClick={handleLike} disabled={isPending}>
      {optimisticPost.likedByUser ? "Unlike" : "Like"}
      ({optimisticPost.likes})
    </button>
  );
}

25. How does the Next.js build process work?

bash
next build

The build process:

  1. 1Type checking — runs TypeScript compiler
  2. 2Linting — runs ESLint
  3. 3Compilation — compiles all pages with SWC (Rust-based, 17x faster than Babel)
  4. 4Route analysis — categorizes each route as Static, Dynamic, or ISR
  5. 5Pre-rendering — generates HTML for all static routes
  6. 6Bundle analysis — creates optimized JS chunks with tree-shaking

Build output:

Route (app)                    Size     First Load JS
┌ ○ /                          5.2 kB   87.1 kB
├ ○ /about                     1.2 kB   83.1 kB
├ ● /blog/[slug]               3.5 kB   85.4 kB   (ISR: 3600s)
├ λ /dashboard                 8.1 kB   90.0 kB   (Dynamic: SSR)
└ ○ /blog                      4.1 kB   86.0 kB

○  Static    ●  ISR    λ  Dynamic

Analyzing bundle size:

bash
# Install
npm install @next/bundle-analyzer

# next.config.mjs
import bundleAnalyzer from "@next/bundle-analyzer";
const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === "true",
});
export default withBundleAnalyzer({});

# Run
ANALYZE=true npm run build

26. What is `next.config.mjs` and what are the most important options?

mjs
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Enable React's strict mode — double-invokes effects in dev to catch bugs
  reactStrictMode: true,

  // Redirect rules
  async redirects() {
    return [
      {
        source: "/old-blog/:slug",
        destination: "/blog/:slug",
        permanent: true, // 308 vs 307
      },
    ];
  },

  // Rewrite rules (proxy without URL change)
  async rewrites() {
    return [
      {
        source: "/api/v1/:path*",
        destination: "https://internal-api.com/:path*",
      },
    ];
  },

  // Custom response headers
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          { key: "X-Frame-Options", value: "DENY" },
          { key: "X-Content-Type-Options", value: "nosniff" },
        ],
      },
    ];
  },

  // Experimental features
  experimental: {
    ppr: true, // Partial Pre-Rendering
  },

  // Webpack customization
  webpack(config) {
    config.plugins.push(new MyPlugin());
    return config;
  },
};

export default nextConfig;

27. What is Partial Pre-Rendering (PPR)?

PPR is an experimental Next.js 14+ rendering model that combines static and dynamic content in a single page — without forcing the whole page to be dynamic.

tsx
// app/product/[id]/page.tsx
import { Suspense } from "react";
import { experimental_ppr } from "next";

// Enable PPR for this route
export const experimental_ppr = true;

export default async function ProductPage({ params }) {
  return (
    <div>
      {/*
        Static shell — rendered at build time, served instantly from CDN.
        Next.js pre-renders everything outside Suspense boundaries statically.
      */}
      <ProductLayout>
        <StaticProductInfo params={params} />

        {/*
          Dynamic hole — filled with streaming content per request.
          Suspense boundary = the "hole" in the static shell.
        */}
        <Suspense fallback={<PriceSkeleton />}>
          <DynamicPrice productId={params.id} /> {/* Personalized, no-cache */}
        </Suspense>

        <Suspense fallback={<ReviewsSkeleton />}>
          <UserReviews productId={params.id} /> {/* Fresh per request */}
        </Suspense>
      </ProductLayout>
    </div>
  );
}

Why this matters: Previously you had to choose — static (fast but stale) or dynamic (fresh but slower). PPR gives you the CDN performance of static pages with the freshness of SSR, in the same response.


28. How do you implement TypeScript with Next.js effectively?

tsx
// Strongly typed page props
interface PageProps {
  params: { id: string };
  searchParams: { tab?: "overview" | "reviews"; page?: string };
}

export default async function ProductPage({ params, searchParams }: PageProps) {
  const tab = searchParams.tab ?? "overview";
  const page = Number(searchParams.page ?? "1");
  // ...
}

// generateMetadata — typed return
import type { Metadata } from "next";

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const product = await getProduct(params.id);
  return {
    title: product.name,
    description: product.description,
    openGraph: {
      images: [{ url: product.imageUrl }],
    },
  };
}
ts
// Typed Server Actions
"use server";
import { z } from "zod";

const CreateJobSchema = z.object({
  title: z.string().min(3).max(100),
  company: z.string().min(2),
  salary: z.number().positive().optional(),
});

type ActionState = { success: true; id: string } | { success: false; errors: string[] };

export async function createJob(
  prevState: ActionState,
  formData: FormData
): Promise<ActionState> {
  const result = CreateJobSchema.safeParse({
    title: formData.get("title"),
    company: formData.get("company"),
    salary: formData.get("salary") ? Number(formData.get("salary")) : undefined,
  });

  if (!result.success) {
    return {
      success: false,
      errors: result.error.issues.map((i) => i.message),
    };
  }

  const job = await db.job.create({ data: result.data });
  return { success: true, id: job.id };
}
tsx
// Using typed Server Action with useFormState
"use client";
import { useFormState } from "react-dom";
import { createJob } from "@/app/actions";

export function CreateJobForm() {
  const [state, action] = useFormState(createJob, { success: true, id: "" });

  return (
    <form action={action}>
      {!state.success && (
        <ul>{state.errors.map((e) => <li key={e}>{e}</li>)}</ul>
      )}
      <input name="title" />
      <input name="company" />
      <button type="submit">Create</button>
    </form>
  );
}

29. How do you handle metadata and SEO in Next.js?

tsx
// app/layout.tsx — site-wide defaults
import type { Metadata } from "next";

export const metadata: Metadata = {
  // Template: page titles will render as "Page Title | My Site"
  title: {
    default: "My Site",
    template: "%s | My Site",
  },
  description: "Default site description",
  metadataBase: new URL("https://mysite.com"),

  // Open Graph
  openGraph: {
    type: "website",
    siteName: "My Site",
  },

  // Twitter Cards
  twitter: {
    card: "summary_large_image",
    creator: "@myhandle",
  },

  // Robots
  robots: {
    index: true,
    follow: true,
  },
};
tsx
// app/blog/[slug]/page.tsx — dynamic metadata per page
export async function generateMetadata(
  { params }: { params: { slug: string } }
): Promise<Metadata> {
  const post = await getPost(params.slug);

  return {
    title: post.title, // Renders as "Post Title | My Site" due to template
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: "article",
      publishedTime: post.publishedAt.toISOString(),
      authors: [post.author.name],
      images: [
        {
          url: post.ogImage ?? "/og-default.png",
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    // Canonical URL
    alternates: {
      canonical: `/blog/${params.slug}`,
    },
  };
}

Structured data (JSON-LD) for rich results:

tsx
export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);

  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    headline: post.title,
    description: post.excerpt,
    datePublished: post.publishedAt,
    author: { "@type": "Person", name: post.author.name },
    image: post.ogImage,
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <article>{/* ... */}</article>
    </>
  );
}

30. How do you generate a sitemap in Next.js?

ts
// app/sitemap.ts
import type { MetadataRoute } from "next";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await db.post.findMany({
    select: { slug: true, updatedAt: true },
  });

  const postUrls = posts.map((post) => ({
    url: `https://mysite.com/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: "weekly" as const,
    priority: 0.7,
  }));

  return [
    {
      url: "https://mysite.com",
      lastModified: new Date(),
      changeFrequency: "daily",
      priority: 1,
    },
    {
      url: "https://mysite.com/blog",
      lastModified: new Date(),
      changeFrequency: "daily",
      priority: 0.8,
    },
    ...postUrls,
  ];
}
ts
// app/robots.ts
import type { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      { userAgent: "*", allow: "/" },
      { userAgent: "GPTBot", allow: "/" }, // Allow AI citation crawlers
    ],
    sitemap: "https://mysite.com/sitemap.xml",
  };
}

Testing

31. How do you test Next.js applications?

Unit tests with Vitest:

ts
// lib/utils.test.ts
import { describe, it, expect } from "vitest";
import { formatSalary, slugify } from "./utils";

describe("formatSalary", () => {
  it("formats USD correctly", () => {
    expect(formatSalary(120000, "USD")).toBe("$120,000/yr");
  });

  it("handles missing salary", () => {
    expect(formatSalary(null)).toBe("Salary not disclosed");
  });
});

Testing Server Components with React Testing Library:

tsx
// __tests__/JobCard.test.tsx
import { render, screen } from "@testing-library/react";
import { JobCard } from "@/components/JobCard";

// Mock the database module
vi.mock("@/lib/db", () => ({
  db: { job: { findUnique: vi.fn() } },
}));

describe("JobCard", () => {
  it("renders job title and company", () => {
    render(
      <JobCard
        job={{ id: "1", title: "Senior Engineer", company: "Acme Inc" }}
      />
    );

    expect(screen.getByText("Senior Engineer")).toBeInTheDocument();
    expect(screen.getByText("Acme Inc")).toBeInTheDocument();
  });
});

E2E testing with Playwright:

ts
// e2e/auth.spec.ts
import { test, expect } from "@playwright/test";

test("unauthenticated user is redirected to login", async ({ page }) => {
  await page.goto("/dashboard");
  await expect(page).toHaveURL("/login?next=/dashboard");
});

test("authenticated user sees their dashboard", async ({ page, context }) => {
  // Set session cookie directly
  await context.addCookies([
    { name: "session", value: testSessionToken, domain: "localhost" },
  ]);

  await page.goto("/dashboard");
  await expect(page.getByRole("heading", { name: /welcome/i })).toBeVisible();
});

32. How do you mock `next/navigation` in tests?

tsx
// __tests__/SearchComponent.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { SearchComponent } from "@/components/SearchComponent";

// Mock the entire next/navigation module
const mockPush = vi.fn();
const mockReplace = vi.fn();

vi.mock("next/navigation", () => ({
  useRouter: () => ({
    push: mockPush,
    replace: mockReplace,
    back: vi.fn(),
    refresh: vi.fn(),
  }),
  usePathname: () => "/dashboard",
  useSearchParams: () => new URLSearchParams("q=react"),
}));

test("submitting search navigates to results", () => {
  render(<SearchComponent />);

  const input = screen.getByRole("searchbox");
  fireEvent.change(input, { target: { value: "nextjs" } });
  fireEvent.submit(input.closest("form")!);

  expect(mockReplace).toHaveBeenCalledWith("/dashboard?q=nextjs");
});

Common Interview Scenarios

33. How would you implement infinite scroll in Next.js?

tsx
// app/jobs/page.tsx — Server Component with initial data
export default async function JobsPage() {
  const initialJobs = await getJobs({ page: 1, limit: 20 });
  return <InfiniteJobList initialJobs={initialJobs} />;
}
tsx
// components/InfiniteJobList.tsx — Client Component
"use client";
import { useState, useEffect, useRef, useCallback } from "react";

export function InfiniteJobList({ initialJobs }) {
  const [jobs, setJobs] = useState(initialJobs);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const [loading, setLoading] = useState(false);
  const sentinelRef = useRef<HTMLDivElement>(null);

  const loadMore = useCallback(async () => {
    if (loading || !hasMore) return;

    setLoading(true);
    const nextPage = page + 1;
    const newJobs = await fetch(`/api/jobs?page=${nextPage}`).then((r) => r.json());

    if (newJobs.length === 0) {
      setHasMore(false);
    } else {
      setJobs((prev) => [...prev, ...newJobs]);
      setPage(nextPage);
    }
    setLoading(false);
  }, [loading, hasMore, page]);

  // Intersection Observer — triggers loadMore when sentinel enters viewport
  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => { if (entry.isIntersecting) loadMore(); },
      { rootMargin: "200px" } // Start loading 200px before sentinel is visible
    );

    if (sentinelRef.current) observer.observe(sentinelRef.current);
    return () => observer.disconnect();
  }, [loadMore]);

  return (
    <div>
      {jobs.map((job) => <JobCard key={job.id} job={job} />)}
      <div ref={sentinelRef} />
      {loading && <LoadingSpinner />}
      {!hasMore && <p>No more jobs</p>}
    </div>
  );
}

34. How would you implement a multi-tenant architecture in Next.js?

ts
// middleware.ts
import { NextResponse } from "next/server";

export function middleware(request) {
  const hostname = request.headers.get("host") ?? "";

  // app.mysite.com → /app
  // [tenant].mysite.com → /[tenant]
  // mysite.com → main site

  const isApp = hostname.startsWith("app.");
  const tenantMatch = hostname.match(/^([^.]+)\.mysite\.com$/);

  if (isApp) {
    return NextResponse.rewrite(
      new URL(`/app${request.nextUrl.pathname}`, request.url)
    );
  }

  if (tenantMatch && tenantMatch[1] !== "www") {
    const tenant = tenantMatch[1];
    // Forward tenant to the app via header
    const response = NextResponse.rewrite(
      new URL(`/tenant/${tenant}${request.nextUrl.pathname}`, request.url)
    );
    response.headers.set("x-tenant", tenant);
    return response;
  }

  return NextResponse.next();
}
tsx
// app/tenant/[tenant]/page.tsx
import { headers } from "next/headers";

export default async function TenantPage({ params }) {
  const tenant = params.tenant;
  const tenantConfig = await getTenantConfig(tenant);

  if (!tenantConfig) notFound();

  return <TenantDashboard config={tenantConfig} />;
}

35. How do you handle form validation with Server Actions?

tsx
// app/actions.ts
"use server";

import { z } from "zod";
import { redirect } from "next/navigation";

const ContactSchema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Invalid email address"),
  message: z.string().min(10, "Message must be at least 10 characters"),
});

export type FormState = {
  errors?: {
    name?: string[];
    email?: string[];
    message?: string[];
    _form?: string[];
  };
  success?: boolean;
};

export async function submitContact(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const validatedFields = ContactSchema.safeParse({
    name: formData.get("name"),
    email: formData.get("email"),
    message: formData.get("message"),
  });

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    };
  }

  try {
    await sendEmail(validatedFields.data);
    return { success: true };
  } catch (error) {
    return {
      errors: { _form: ["Failed to send message. Please try again."] },
    };
  }
}
tsx
// components/ContactForm.tsx
"use client";
import { useFormState, useFormStatus } from "react-dom";
import { submitContact, type FormState } from "@/app/actions";

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "Sending..." : "Send Message"}
    </button>
  );
}

export function ContactForm() {
  const [state, action] = useFormState(submitContact, {});

  if (state.success) {
    return <p>Message sent successfully!</p>;
  }

  return (
    <form action={action} noValidate>
      <div>
        <label htmlFor="name">Name</label>
        <input id="name" name="name" type="text" />
        {state.errors?.name && (
          <p role="alert" className="text-red-500">
            {state.errors.name[0]}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="email">Email</label>
        <input id="email" name="email" type="email" />
        {state.errors?.email && (
          <p role="alert">{state.errors.email[0]}</p>
        )}
      </div>

      <div>
        <label htmlFor="message">Message</label>
        <textarea id="message" name="message" />
        {state.errors?.message && (
          <p role="alert">{state.errors.message[0]}</p>
        )}
      </div>

      {state.errors?._form && (
        <p role="alert" className="text-red-500">
          {state.errors._form[0]}
        </p>
      )}

      <SubmitButton />
    </form>
  );
}

36. How do you deploy a Next.js app and what are the deployment options?

Vercel (zero-config):

bash
npm install -g vercel
vercel # Follow the prompts — automatic detection and deployment

Docker (self-hosted):

dockerfile
# Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

# Use standalone output for minimal image size
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

EXPOSE 3000
CMD ["node", "server.js"]
mjs
// next.config.mjs — required for Docker standalone
export default {
  output: "standalone",
};

Static export (no server needed):

mjs
// next.config.mjs
export default {
  output: "export",
  // All pages must be statically generateable — no SSR, no API routes
};
bash
next build # Outputs to /out directory
# Deploy /out to any static host: S3, GitHub Pages, Cloudflare Pages

37. What are the differences between `layout.tsx` and `template.tsx`?

tsx
// layout.tsx — persists across navigation, does NOT remount
// State is preserved when navigating between children
export default function DashboardLayout({ children }) {
  // This component mounts once. useEffect runs once.
  // Navigating between /dashboard/jobs and /dashboard/settings
  // does NOT remount this layout.
  return (
    <div>
      <Sidebar />  {/* Stays mounted — sidebar state persists */}
      <main>{children}</main>
    </div>
  );
}
tsx
// template.tsx — remounts on EVERY navigation
// Use for: animations, useEffect on each navigation, per-route state reset
export default function Template({ children }) {
  // useEffect runs on EVERY navigation to a child route
  // Entering animation will replay on each route change
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0 }}
    >
      {children}
    </motion.div>
  );
}

Rule of thumb: Use layout.tsx by default. Use template.tsx when you need enter/exit animations or when you want useEffect to fire on every route change within the segment.


38. How do you implement role-based access control (RBAC)?

ts
// lib/rbac.ts
type Role = "user" | "admin" | "moderator";
type Permission = "read:jobs" | "write:jobs" | "delete:jobs" | "manage:users";

const rolePermissions: Record<Role, Permission[]> = {
  user: ["read:jobs"],
  moderator: ["read:jobs", "write:jobs"],
  admin: ["read:jobs", "write:jobs", "delete:jobs", "manage:users"],
};

export function hasPermission(role: Role, permission: Permission): boolean {
  return rolePermissions[role]?.includes(permission) ?? false;
}
ts
// middleware.ts — coarse-grained RBAC at the Edge
export async function middleware(request) {
  const token = request.cookies.get("session")?.value;
  const payload = token ? await verifyToken(token) : null;

  if (request.nextUrl.pathname.startsWith("/admin")) {
    if (!payload || payload.role !== "admin") {
      return NextResponse.redirect(new URL("/403", request.url));
    }
  }

  return NextResponse.next();
}
tsx
// Fine-grained RBAC in Server Components
async function AdminPanel() {
  const session = await getServerSession();

  if (!hasPermission(session.user.role, "manage:users")) {
    return <p>You don&apos;t have permission to view this.</p>;
  }

  const users = await getAllUsers();
  return <UserManagement users={users} />;
}

39. How does Next.js handle CSS and styling?

tsx
// 1. CSS Modules — scoped by default
// styles/Button.module.css
.button { background: blue; color: white; }
.large { font-size: 1.25rem; }

// Button.tsx
import styles from "./Button.module.css";
<button className={`${styles.button} ${isLarge ? styles.large : ""}`}>

// With clsx for conditional classes
import clsx from "clsx";
<button className={clsx(styles.button, { [styles.large]: isLarge })}>
tsx
// 2. Tailwind CSS — most popular in Next.js ecosystem
<button className={clsx(
  "px-4 py-2 rounded-md font-medium transition-colors",
  variant === "primary" && "bg-blue-600 text-white hover:bg-blue-700",
  variant === "secondary" && "bg-gray-100 text-gray-800 hover:bg-gray-200",
  disabled && "opacity-50 cursor-not-allowed"
)}>

// 3. CSS-in-JS — only some work with App Router
// styled-components requires 'use client' and a registry setup
// Vanilla Extract works at build time — compatible with RSC

Global styles:

tsx
// app/layout.tsx
import "@/styles/globals.css"; // Only import in root layout

export default function RootLayout({ children }) {
  return <html><body>{children}</body></html>;
}

40. What are common performance pitfalls in Next.js and how do you avoid them?

Pitfall 1: Putting everything in Client Components

tsx
// ❌ Unnecessary "use client" — loses RSC benefits
"use client";
async function ProductList() {
  // This doesn't use any client features, yet ships JS to browser
  const products = await getProducts();
  return <ul>{products.map(p => <li>{p.name}</li>)}</ul>;
}

// ✅ Server Component — zero client JS for this component
async function ProductList() {
  const products = await getProducts();
  return <ul>{products.map(p => <li>{p.name}</li>)}</ul>;
}

Pitfall 2: Not using loading.tsx for slow pages

// ❌ User sees blank screen for 800ms
// ✅ Add loading.tsx — instant skeleton
app/dashboard/
├── page.tsx        → slow (800ms database query)
└── loading.tsx     → instant skeleton shown immediately

Pitfall 3: Waterfall data fetching

tsx
// ❌ Sequential — total time = 200ms + 300ms + 400ms = 900ms
async function Page() {
  const user = await getUser();
  const posts = await getPosts(user.id);
  const comments = await getComments(posts[0].id);
}

// ✅ Parallel — total time = max(200ms, 300ms, 400ms) = 400ms
async function Page() {
  const [user, posts, comments] = await Promise.all([
    getUser(),
    getPosts(),
    getComments(),
  ]);
}

Pitfall 4: Not providing image dimensions

tsx
// ❌ Causes CLS — browser doesn't know space to reserve
<Image src="/photo.jpg" alt="Photo" />

// ✅ Dimensions reserved — CLS = 0
<Image src="/photo.jpg" alt="Photo" width={800} height={600} />

Pitfall 5: Importing heavy libraries on the client

tsx
// ❌ Ships 200KB moment.js to every user
"use client";
import moment from "moment";
const formatted = moment(date).format("MMMM DD, YYYY");

// ✅ Format on server — zero client bundle cost
async function PostDate({ date }: { date: Date }) {
  // Runs on server — Intl is free, no bundle
  const formatted = new Intl.DateTimeFormat("en-US", {
    month: "long", day: "numeric", year: "numeric"
  }).format(date);
  return <time dateTime={date.toISOString()}>{formatted}</time>;
}

41. How do you implement a real-time feature in Next.js?

tsx
// Route Handler for Server-Sent Events (SSE)
// app/api/notifications/route.ts
export async function GET(request: Request) {
  const stream = new ReadableStream({
    start(controller) {
      const encoder = new TextEncoder();

      // Subscribe to notifications for this user
      const unsubscribe = subscribeToNotifications(userId, (notification) => {
        const data = `data: ${JSON.stringify(notification)}\n\n`;
        controller.enqueue(encoder.encode(data));
      });

      // Cleanup when client disconnects
      request.signal.addEventListener("abort", () => {
        unsubscribe();
        controller.close();
      });
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive",
    },
  });
}
tsx
// Client Component consuming SSE
"use client";
import { useEffect, useState } from "react";

export function NotificationFeed() {
  const [notifications, setNotifications] = useState([]);

  useEffect(() => {
    const eventSource = new EventSource("/api/notifications");

    eventSource.onmessage = (event) => {
      const notification = JSON.parse(event.data);
      setNotifications((prev) => [notification, ...prev]);
    };

    eventSource.onerror = () => eventSource.close();

    return () => eventSource.close();
  }, []);

  return (
    <ul>
      {notifications.map((n) => (
        <li key={n.id}>{n.message}</li>
      ))}
    </ul>
  );
}

For WebSockets, use a dedicated service (Pusher, Ably, Socket.io) since Next.js serverless functions don't support persistent connections.


42. What is the `cache()` function in React and when do you use it in Next.js?

tsx
// lib/data.ts
import { cache } from "react";

// cache() memoizes the result for the duration of a single request.
// Multiple calls with the same argument in the same render pass
// only execute the function once.
export const getUser = cache(async (id: string) => {
  console.log(`Fetching user ${id}...`); // Logs once even if called 5 times
  return db.user.findUnique({ where: { id } });
});

// Multiple Server Components can call getUser(userId)
// — only one database query is made per request

When to use cache():

  • When using ORMs, database clients, or any async function that is NOT fetch() (because fetch() is automatically deduplicated by Next.js)
  • When the same data is needed by multiple components in the same render tree
  • When you want to preload data (call without awaiting before the component that needs it)
tsx
// Preloading pattern
async function UserProfilePage({ params }) {
  // Start fetching immediately — don't wait for UserHeader to render
  getUser(params.id); // Fire and forget — cache() stores the result

  return (
    <div>
      <UserHeader userId={params.id} /> {/* Will use cached result */}
      <UserPosts userId={params.id} />  {/* Will use cached result */}
    </div>
  );
}

43. How do you handle internationalization (i18n) in Next.js?

mjs
// next.config.mjs — built-in i18n (Pages Router)
export default {
  i18n: {
    locales: ["en", "es", "pt"],
    defaultLocale: "en",
    localeDetection: true,
  },
};

App Router i18n (using next-intl):

app/
└── [locale]/
    ├── layout.tsx
    └── page.tsx
tsx
// middleware.ts — redirect to correct locale
import createMiddleware from "next-intl/middleware";

export default createMiddleware({
  locales: ["en", "es", "pt"],
  defaultLocale: "en",
  localePrefix: "as-needed", // /about (EN), /es/about (ES)
});

export const config = {
  matcher: ["/((?!api|_next|.*\\..*).*)", "/"],
};
tsx
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";

export default async function LocaleLayout({ children, params: { locale } }) {
  const messages = await getMessages();

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}
tsx
// Server Component translation
import { useTranslations } from "next-intl";

export function HeroSection() {
  const t = useTranslations("Hero");
  return (
    <h1>{t("title")}</h1> // messages/en.json: { "Hero": { "title": "..." } }
  );
}

44. What is `unstable_noStore` and when would you use it?

ts
import { unstable_noStore as noStore } from "next/cache";

async function getLiveData() {
  noStore(); // Explicitly opt out of all caching for this call

  // Equivalent to fetch(..., { cache: "no-store" }) but works
  // with non-fetch data sources like ORM calls
  const data = await db.liveMetrics.findFirst({
    orderBy: { timestamp: "desc" },
  });
  return data;
}

Use noStore() when:

  • You need data that is fresh on every request
  • You're using an ORM or SDK (not fetch) and can't pass cache: "no-store"
  • You want to explicitly document "this component opts out of caching" without relying on cache: "no-store" on a fetch call

45. How do you debug Next.js performance issues?

bash
# 1. Analyze JavaScript bundle
ANALYZE=true npm run build
# Opens webpack-bundle-analyzer in browser — spot large dependencies

# 2. Check what's being rendered where
# App Router: add console.log to Server Components — logs appear in TERMINAL
# App Router: add console.log to Client Components — logs appear in BROWSER console

# 3. TypeScript strict mode catches client/server boundary violations at compile time

# 4. Enable React DevTools Profiler (Client Components only)

# 5. Speed Insights (Vercel) — real user Core Web Vitals
tsx
// Debug: check if a component is Server or Client
// Server Components run during build/request — log appears in terminal
// Client Components run in browser — log appears in DevTools
async function MyComponent() {
  console.log("I am a Server Component"); // Terminal
  return <div />;
}

"use client";
function MyClientComponent() {
  console.log("I am a Client Component"); // Browser DevTools
  return <div />;
}
tsx
// The "server-only" package — prevents server code from being imported in Client Components
import "server-only"; // Throws build error if imported in a Client Component

export async function getSecretData() {
  return db.secrets.findMany(); // This function cannot accidentally end up client-side
}

What Interviewers Are Really Testing

Beyond the individual questions, experienced Next.js interviewers are looking for three things:

1. Mental model clarity — Do you understand the rendering spectrum? Can you explain *why* you'd choose SSG over SSR for a given page, not just *what* they are? The "it depends" answer is correct, but you need to complete the sentence with the right criteria.

2. Boundary awareness — Do you know where the server ends and the client begins? Can you explain why "use client" exists, why Server Components can't use hooks, and how to pass data across the boundary correctly? This is the hardest concept for developers new to the App Router.

3. Trade-off reasoning — Next.js gives you many tools. The signal interviewers look for is whether you default to the most performant option (Server Component, SSG) and only reach for the heavier option (Client Component, SSR) when there's a concrete reason. Reflexively adding "use client" or getServerSideProps everywhere is a red flag.

The strongest interview answers pair a concept with a real scenario: "I've used ISR for product pages because inventory changes every few hours and we don't need real-time accuracy, but the static performance matters for SEO. We wired up on-demand revalidation to our CMS webhook so the page updates within seconds of an editor publishing."

That combination — understanding, judgment, and experience — is what separates candidates.


*Questions 1-10 cover foundations that appear in almost every Next.js interview. Questions 11-25 cover architecture and the App Router patterns that senior roles require. Questions 26-45 cover the operational, testing, and edge-case knowledge that separates strong mid-level from senior engineers.*

FAQ

What is the most important Next.js concept to understand for interviews?+

React Server Components and the client/server boundary. Most interview questions eventually connect back to understanding which code runs on the server, which runs in the browser, and how to pass data across that boundary correctly. Interviewers use this to gauge whether you understand the App Router's mental model inversion — the default is now server, and you opt into the client with 'use client'.

What is the difference between getServerSideProps and the App Router equivalent?+

In the Pages Router, getServerSideProps is an exported async function that runs on the server per request and passes data as props to the page component. In the App Router, you simply make your page component async and fetch data directly inside it using await — no special export needed. For SSR behavior (no caching), use fetch() with { cache: 'no-store' } or call unstable_noStore(). The App Router approach is cleaner because data fetching is co-located with the component that uses it.

When should you use 'use client' in a Next.js App Router project?+

Add 'use client' only when your component needs browser-specific APIs (window, document, localStorage), React hooks that require client state or effects (useState, useEffect, useContext, useRef), or event handlers (onClick, onSubmit, onChange). Everything else should default to Server Components. A common mistake is adding 'use client' to components that only need to render static output — this ships unnecessary JavaScript to the browser.

What is the difference between ISR and on-demand revalidation?+

ISR (Incremental Static Regeneration) with a revalidate time (e.g., revalidate: 60) regenerates a page in the background when a request comes in after the specified number of seconds has elapsed — the stale page is still served until the new one is ready. On-demand revalidation using revalidatePath() or revalidateTag() lets you trigger regeneration immediately when data changes — for example, from a CMS webhook. On-demand revalidation is more precise and results in fresher content, but requires an endpoint that your data source can call.

What are Server Actions and how are they different from API routes?+

Server Actions are async functions marked with 'use server' that run on the server but can be called directly from Client Components — including as HTML form action attributes. They eliminate the need to write a separate API endpoint for mutations. The key differences from API routes: Server Actions can be passed directly as form actions (progressive enhancement — they work without JavaScript), they co-locate mutation logic with the UI, and they automatically secure the server/client boundary with a secret token. API routes remain useful for webhooks, third-party callbacks, and when you need full HTTP control over headers and status codes.

How do you prevent a Server Component from being cached when you need fresh data?+

There are three approaches. First, use fetch() with { cache: 'no-store' } for any fetch calls inside the component. Second, use the unstable_noStore() function at the top of a Server Component or data function when you're not using fetch (e.g., direct ORM calls). Third, export dynamic = 'force-dynamic' from a page or layout file to opt the entire route out of static generation. The first two are preferred for granular control — they let you make one part of a page dynamic without making the whole route dynamic.

What is Partial Pre-Rendering (PPR) and why does it matter?+

PPR is an experimental Next.js rendering model that lets a single page have both a statically-generated shell (served instantly from a CDN) and dynamic holes filled with streamed content per request. Previously, any dynamic data on a page forced the entire page to be server-rendered on every request. With PPR, you wrap dynamic sections in Suspense boundaries — Next.js pre-renders everything outside those boundaries at build time and streams the dynamic parts in separately. This gives you CDN-level performance for the page shell with per-request freshness for personalized or real-time sections.

How does Next.js handle TypeScript out of the box?+

Next.js has zero-configuration TypeScript support — create a tsconfig.json (or just rename a file to .tsx) and Next.js automatically configures the compiler. It provides built-in types for all special files (page props, layout props, route handler params, generateMetadata, generateStaticParams) via the 'next' package. The Metadata type for SEO, NextRequest/NextResponse for route handlers, and PageProps types are all available without additional setup. For the strongest setup, pair this with @t3-oss/env-nextjs for type-safe environment variables and zod for runtime validation in Server Actions.

Related articles

How to Answer Conflict-With-a-Coworker Interview Questions

Learn how to answer conflict-with-a-coworker interview questions with real examples and proven techniques. Stand out in tech and remote job interviews.

How to Answer 'Why Do You Want to Work Here' in Interviews

Discover expert strategies for answering 'why do you want to work here,' tailored for remote tech roles and dollar opportunities. Real, practical interview tips.

Frontend Developer Interview Questions and How to Answer Them (50+)

Complete SEO article covering 54 frontend developer interview questions with detailed answers, real code snippets across HTML, CSS, JavaScript, React, TypeScript, accessibility, security, build tools, and testing.

Full-Stack Developer Interview Questions: How to Answer Like a Pro (45+)

Comprehensive full-stack developer interview guide with 46 numbered questions covering JavaScript/TypeScript, React, CSS, REST APIs, databases, Node.js, system design, security, testing, DevOps, and advanced architecture topics. Each answer includes working code examples and production-level context.

Prepare for your real interview

Paste your job link: we research who's interviewing you and rehearse you live.

Start free →

Have an interview coming up? Install the live copilot →

InterviewHack.ai

Prepare for the exact interview: who's interviewing you, a tailored CV, and a real coach.

Product

JobsFree ATS checkerInterview-English checkSalary checkLATAM salary reportFree coursesBlogTailored CVSpoken practiceIt's free

Remote jobs

ReactPythonFull-StackLATAMArgentinaMexicoSee all →

Prepare

Spoken practiceFrontendBackendAI EngineerBy companySell with your CV

Company

For employersAboutContactPrivacyTerms

© 2026 InterviewHack.ai · Your CV is yours. Never used to train anything. · A product of IA-PTY