Next.js interview questions covering App Router, SSR, SSG, server components, API routes, caching, auth, deployment, and performance.
Next.js is a React framework for building full-stack web applications. It adds routing, server rendering, static generation, API routes or route handlers, image optimization, metadata handling, caching, and deployment features on top of React. Example: a product site can use static pages for marketing, server-rendered pages for search results, and route handlers for form submissions.
The App Router is the modern routing system based on the app directory. It supports layouts, nested routes, Server Components, loading states, error boundaries, route handlers, streaming, and Server Actions. Routes are created with folders and page files.
app/
layout.js
page.js
products/
page.js
[id]/
page.js
Pages Router uses the pages directory and file-based routing with functions like getServerSideProps and getStaticProps. App Router uses the app directory, React Server Components, layouts, route handlers, and new caching patterns. For new projects, App Router is usually preferred, while older apps may still use Pages Router.
A React Server Component renders on the server and can directly access server-side resources such as databases, files, or private environment variables. It does not send its JavaScript to the browser, which can reduce bundle size. Example: a product detail page can fetch product data on the server and render HTML without exposing database code.
A Client Component runs in the browser and is required when you need state, effects, browser APIs, or event handlers. Add "use client" at the top of the file. Use Client Components only where needed to keep browser JavaScript small.
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Use Server Components for data fetching, static content, secure server access, and markup that does not need browser interactivity. Use Client Components for forms with client state, modals, tabs, charts, event handlers, and browser-only APIs. Example: render the product page on the server, but make the add-to-cart button a Client Component.
SSR, or Server-Side Rendering, generates HTML on the server for each request or for dynamic data paths. It is useful when content must be fresh per request, such as authenticated dashboards, personalized pages, or frequently changing search results.
SSG, or Static Site Generation, pre-renders pages at build time. It is useful for pages that do not change often, such as docs, blogs, landing pages, and product category pages. Static pages are fast because they can be served from a CDN.
ISR, or Incremental Static Regeneration, lets static pages be regenerated after a time interval or on demand. It is useful when you want static speed but need periodic freshness. Example: a product catalog page can revalidate every 10 minutes.
export const revalidate = 600;
export default async function ProductsPage() {
const products = await fetchProducts();
return <ProductGrid products={products} />;
}
Choose SSG for mostly static content, ISR for static content that needs scheduled or on-demand freshness, and SSR for request-specific content. Example: a blog post can use SSG, a pricing page can use ISR, and a user dashboard should use SSR because it depends on the logged-in user.
File-based routing means folders and files define URL routes. In App Router, a route becomes publicly accessible when it contains a page.js or page.tsx file. Example: app/about/page.js maps to /about.
Dynamic routes use bracket syntax for variable segments. Example: app/products/[id]/page.js matches /products/10 and receives params.id. This is useful for product pages, blog posts, user profiles, and category pages.
export default function ProductPage({ params }) {
return <h1>Product ID: {params.id}</h1>;
}
layout.js defines shared UI for a route segment and its children. Layouts preserve state during navigation and avoid re-rendering shared shells. Example: a dashboard layout can include sidebar navigation while child pages change inside it.
loading.js defines an instant loading UI for a route segment. It works with React Suspense and is shown while route content is loading. Example: show a skeleton table while a dashboard page fetches server data.
export default function Loading() {
return <div className="skeleton">Loading products...</div>;
}
error.js defines an error boundary for a route segment. It must be a Client Component because it handles reset interactions. Example: if a product page fails to load, error.js can show a retry button instead of breaking the whole app.
'use client';
export default function Error({ error, reset }) {
return <button onClick={reset}>Try again</button>;
}
not-found.js defines the UI shown when a route calls notFound() or when a page does not exist. Example: a product page can call notFound() if the product ID is invalid, showing a custom 404 page.
Route handlers define server endpoints inside the app directory using route.js or route.ts. They replace many API route use cases in App Router. Example: app/api/contact/route.js can handle POST requests from a contact form.
export async function POST(request) {
const body = await request.json();
return Response.json({ received: body.email });
}
Route handlers expose HTTP endpoints and are useful for APIs, webhooks, and external clients. Server Actions are server functions called from React components or forms. Example: use a route handler for a Stripe webhook, but a Server Action for saving a contact form from your own page.
Server Actions let you run server-side code directly from forms or components. They are useful for mutations such as saving a form, updating a profile, or creating an order. They must validate input and permissions on the server.
'use server';
export async function createPost(formData) {
const title = formData.get('title');
// validate, authorize, save to database
}
In App Router, Server Components can fetch data directly with async functions. You can fetch from APIs, databases, or internal services on the server. Example: an async page component can await fetchProducts() before returning JSX.
Next.js extends fetch with caching and revalidation options. You can cache responses, disable caching, or revalidate after a time. Example: use cache: "no-store" for per-request data and next: { revalidate: 60 } for data that can be refreshed every minute.
await fetch('https://api.example.com/products', {
next: { revalidate: 60 }
});
await fetch('https://api.example.com/account', {
cache: 'no-store'
});
revalidatePath invalidates cached data for a specific route. It is useful after mutations. Example: after creating a blog post with a Server Action, call revalidatePath("/blog") so the blog list refreshes.
revalidateTag invalidates cached fetches grouped by a tag. It is useful when many pages depend on the same data. Example: tag product fetches with "products" and revalidate that tag after inventory changes.
await fetch('/api/products', {
next: { tags: ['products'] }
});
// later in a server action
revalidateTag('products');
generateStaticParams tells Next.js which dynamic routes to pre-render at build time. Example: for app/blog/[slug]/page.js, return popular blog slugs so those pages are generated statically.
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
generateMetadata creates dynamic SEO metadata for a route. It can fetch data and return title, description, Open Graph data, and canonical info. Example: product pages can generate unique title and description from product data.
export async function generateMetadata({ params }) {
const product = await getProduct(params.id);
return { title: product.name, description: product.summary };
}
Export a metadata object from a layout or page. This is useful for static titles and descriptions. Example: export const metadata = { title: "About", description: "About our company" }.
Middleware runs before a request completes and can redirect, rewrite, set headers, or enforce simple checks. It is useful for locale routing, redirects, auth gates, and A/B testing, but heavy business logic should stay in server code.
import { NextResponse } from 'next/server';
export function middleware(request) {
const loggedIn = request.cookies.has('session');
if (!loggedIn) return NextResponse.redirect(new URL('/login', request.url));
return NextResponse.next();
}
export const config = { matcher: ['/dashboard/:path*'] };
The Edge Runtime runs code closer to users with low latency, but it has fewer Node.js APIs. It is useful for middleware, lightweight personalization, and request routing. Avoid it for code that needs full Node.js modules or long-running work.
The Node.js Runtime supports standard Node.js APIs and is appropriate for database access, file operations, server libraries, and heavier backend logic. Many route handlers and Server Actions use Node.js runtime by default.
Use .env files or deployment environment settings. Variables without NEXT_PUBLIC_ are server-only. Variables prefixed with NEXT_PUBLIC_ are bundled for the browser. Example: DATABASE_URL should be server-only, while NEXT_PUBLIC_SITE_URL can be exposed.
Keep secrets in server-only environment variables, never import server secrets into Client Components, and never prefix secrets with NEXT_PUBLIC_. Example: payment API keys should be used only in route handlers or Server Actions.
Authentication can be implemented with cookies, sessions, JWTs, OAuth providers, or libraries such as NextAuth/Auth.js. Server-side checks should protect pages, Server Actions, and route handlers. Client-side UI checks alone are not security.
Protect it on the server by checking the session in a layout, page, route handler, middleware, or Server Action. Example: a dashboard layout can redirect to /login if getCurrentUser() returns null.
import { redirect } from 'next/navigation';
export default async function DashboardLayout({ children }) {
const user = await getCurrentUser();
if (!user) redirect('/login');
return children;
}
The next/image component optimizes images with resizing, lazy loading, modern formats, and layout stability. Example: use Image for product thumbnails to reduce bandwidth and improve Core Web Vitals.
import Image from 'next/image';
<Image src="/product.jpg" alt="Product" width={600} height={400} />
next/font optimizes fonts by self-hosting them, reducing layout shift and avoiding extra external requests. Example: import a Google font through next/font/google and apply its className to the root layout.
Optimize images, reduce JavaScript, use Server Components, split Client Components, cache data, use proper font loading, avoid layout shift, stream slow content, and measure performance. Example: moving a static product description from a Client Component to a Server Component reduces JS shipped to the browser.
Hydration errors happen when server-rendered HTML does not match client-rendered HTML. Common causes include Date.now(), Math.random(), browser-only APIs, user-specific values, invalid HTML, and conditional rendering that differs between server and client.
Make server and client output match, move browser-only logic into useEffect, use Client Components for interactive browser behavior, avoid random values during render, and check invalid HTML nesting. Example: read localStorage only inside useEffect.
Dynamic import loads a component only when needed, reducing initial bundle size. It is useful for large charts, editors, maps, or modals. Example: load a chart component only on the analytics page.
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('./Chart'), { ssr: false });
Streaming lets the server send parts of the UI as they become ready. It improves perceived performance when some sections are slow. Example: show the page shell immediately while a recommendations section loads behind Suspense.
Suspense lets you show fallback UI while a component or data dependency is loading. In App Router, loading.js and Suspense boundaries help stream UI and avoid blocking the whole page.
Parallel routes let you render multiple route segments at the same time using named slots. They are useful for dashboards, split views, and modals. Example: show analytics and activity feed independently in a dashboard layout.
Forms can be handled with Client Components, route handlers, or Server Actions. For simple server mutations, Server Actions are convenient. Always validate on the server and handle errors clearly.
Read FormData, validate required fields and types, check authorization, then perform the mutation. Example: validate email and message before saving a contact form. Never trust client-side validation alone.
For public data, Server Components can fetch directly. For private API keys, use route handlers or Server Actions so secrets stay on the server. Example: call a payment provider from app/api/checkout/route.js, not from browser code.
A redirect changes the browser URL and sends the user to another location. A rewrite serves different content while keeping the original URL. Example: redirect /old-blog to /blog, but rewrite /docs/latest to the current version internally.
Create a route.js or route.ts file and export HTTP method functions such as GET, POST, PUT, or DELETE. Use Request and Response APIs, validate inputs, and return JSON where appropriate.
Validate input, catch expected failures, return correct HTTP status codes, avoid leaking internal errors, and log server-side details. Example: return 400 for invalid body, 401 for unauthenticated, and 500 for unexpected errors.
A Next.js app can be deployed to Vercel, Node servers, Docker, or supported serverless platforms. Check environment variables, build output, image domains, caching behavior, database connectivity, and runtime compatibility before release.
Common mistakes include making everything a Client Component, exposing secrets with NEXT_PUBLIC_, misunderstanding caching, causing hydration errors, overusing middleware, ignoring image optimization, skipping server-side authorization, and not measuring performance.
Explore 500+ free tutorials across 20+ languages and frameworks.