Next.js interview questions
App Router file conventions
In the App Router folders define routes and special files inside a folder define the UI of that segment.
File Purpose page.tsxThe UI of the route; without it the folder is not publicly accessible layout.tsxShared UI that wraps child segments and keeps its state between navigations template.tsxLike a layout, but remounts on every navigation loading.tsxInstant loading state, wraps the page in <Suspense>error.tsxError boundary for the segment (must be a Client Component) not-found.tsxUI for notFound()and unmatched URLsroute.tsAn API endpoint (Route Handler) instead of a page How they nest
tsx<Layout> <Template> <ErrorBoundary fallback={<Error />}> <Suspense fallback={<Loading />}> <ErrorBoundary fallback={<NotFound />}> <Page /> </ErrorBoundary> </Suspense> </ErrorBoundary> </Template> </Layout>Child segments are rendered inside the parent layout, so layouts nest:
app/layout.tsx->app/blog/layout.tsx->app/blog/[slug]/page.tsx.Note:
error.tsxdoes not catch errors of thelayout.tsxin the same folder, because the boundary sits inside that layout. Errors in the root layout are handled byglobal-error.tsx.updated
did you know it?Dynamic routes
A folder name in square brackets becomes a route parameter.
Folder Matches paramsapp/blog/[slug]/blog/hello{ slug: 'hello' }app/docs/[...path]/docs/a/b{ path: ['a', 'b'] }app/shop/[[...path]]/shopand/shop/a{ path: undefined }or{ path: ['a'] }params is a Promise
Since Next.js 15
paramsandsearchParamsare Promises and have to be awaited:tsx// app/blog/[slug]/page.tsx export default async function Page({ params }: PageProps<'/blog/[slug]'>) { const { slug } = await params; const post = await getPost(slug); if (!post) notFound(); return <Article post={post} />; }generateStaticParams
It returns the list of params to prerender at build time, so these pages are served as static HTML:
tsxexport async function generateStaticParams() { const posts = await getPosts(); return posts.map((p) => ({ slug: p.slug })); }Params that are not in the list are rendered on the first request and can be cached after that. To return 404 for them instead, export
dynamicParams = false.PageProps<'/blog/[slug]'>is a globally available type helper generated by Next.js, soparamsis typed from the route itself.updated
did you know it?Server and Client Components
In the App Router every component is a Server Component by default. The
'use client'directive marks a boundary: the file and everything it imports become part of the client bundle.Server Component Client Component Runs On the server (build or request) On the server for HTML, then hydrates in the browser async/awaitdataYes No State, effects, event handlers No Yes Secrets, DB, file system Yes No Adds JS to the bundle No Yes Passing Server Components into Client ones
A Client Component cannot import a Server Component, but it can receive one as
childrenor another prop:tsx// app/page.tsx (server) <Modal> <Cart /> {/* still rendered on the server */} </Modal> // modal.tsx 'use client'; export function Modal({ children }: { children: React.ReactNode }) { const [open, setOpen] = useState(false); return open ? <dialog open>{children}</dialog> : <button onClick={() => setOpen(true)}>Cart</button>; }Rules of thumb
- Push
'use client'down to the leaves (a button, not the whole page) to keep the bundle small. - Props crossing the boundary must be serializable: no functions (except Server Actions), class instances or Dates with methods.
- Use the
server-onlypackage in modules with secrets so importing them from the client fails the build.
updated
did you know it?- Push
Server Actions
A Server Function is an async function marked with
'use server'that runs on the server but can be called from the client. Used for form submissions and mutations it is called a Server Action.ts// app/actions.ts 'use server'; export async function createPost(formData: FormData) { const session = await auth(); if (!session) throw new Error('Unauthorized'); const title = String(formData.get('title')); await db.post.create({ data: { title, authorId: session.userId } }); updateTag('posts'); }tsx<form action={createPost}> <input name="title" /> <button>Publish</button> </form>Why they are convenient
- No hand-written API route and
fetchcall. - Forms work before hydration (progressive enhancement).
- The response can contain the updated UI after revalidation in one round trip.
useActionStategives the pending state and the returned result.
Security
Every Server Action is a public POST endpoint. Anyone can call it with any arguments, not only through your UI.
- Check authentication and authorization inside every action, not only on the page that renders the form.
- Validate input (e.g. with Zod):
formDataand arguments come from the client. - Do not rely on hidden fields for user ids, take them from the session.
Actions are dispatched one at a time on the client, so they are meant for mutations, not for parallel data fetching.
updated
did you know it?- No hand-written API route and
Cache Components
With
cacheComponents: truecaching is explicit: nothing is cached unless you ask for it with the'use cache'directive.tsimport { cacheLife, cacheTag } from 'next/cache'; export async function getProducts() { 'use cache'; cacheLife('hours'); cacheTag('products'); return db.query('SELECT * FROM products'); }'use cache'can be placed in a data function, in a component or at the top of a file. Arguments and captured variables become the cache key, so different inputs get separate entries.Three kinds of content on a page
Content How Where it ends up Static (no I/O) Nothing to do Prerendered static shell Cached data 'use cache'+cacheLifeStatic shell, revalidated in the background Request data ( cookies(), uncachedfetch)Wrap in <Suspense>Streams in at request time cacheLife profiles
cacheLifesetsstale(client),revalidate(background refresh) andexpire(hard limit). Profiles:seconds,minutes,hours,days,weeks,maxor a custom object. Very short lifetimes are excluded from the prerender and become dynamic.Variants
'use cache: private'- may readcookies()/headers(), cached per user.'use cache: remote'- durable shared cache handler instead of in-memory (useful on serverless).
How it differs from earlier versions
- Next.js 14:
fetchwas cached by default, which surprised many people. - Next.js 15:
fetchand GET Route Handlers stopped being cached by default. - Next.js 16:
'use cache'makes caching opt-in and works for any async code, not onlyfetch.
Values like
Date.now()orMath.random()must be either cached or read afterawait connection()inside<Suspense>, otherwise the dev overlay reports a blocking prerender.updated
did you know it?Revalidation
Cached data is refreshed either by time (
cacheLife) or on demand after a mutation. For on-demand revalidation there are three functions:updateTagrevalidateTagrevalidatePathTarget Entries with a tag Entries with a tag Everything on a route Behavior Expires immediately Stale-while-revalidate Invalidates the path Where Server Actions only Server Actions, Route Handlers Server Actions, Route Handlers Use case User must see their own change Background refresh, a small delay is fine No tags known for the route Example
ts'use server'; import { updateTag } from 'next/cache'; export async function addComment(postId: string, text: string) { await db.comment.create({ data: { postId, text } }); updateTag(`post-${postId}`); // the author sees the comment right away }ts// app/api/cms-webhook/route.ts import { revalidateTag } from 'next/cache'; export async function POST() { revalidateTag('posts', 'max'); // serve stale content while fresh content is generated return Response.json({ ok: true }); }Tips
- Prefer tags over paths: they are precise and don't invalidate unrelated data.
- For content from a CMS use a long
cacheLife('max')+cacheTagand callrevalidateTagfrom a webhook instead of polling by time. router.refresh()only refetches the current route on the client, it does not clear the server cache.
updated
did you know it?Partial Prerendering (PPR)
Earlier a route was either static or dynamic: one
cookies()call made the whole page render on every request. With Partial Prerendering (on by default with Cache Components) a single route is split into two parts:- Static shell - everything that doesn't depend on the request, prerendered at build time and served from a CDN.
- Dynamic holes - parts inside
<Suspense>that read request data and stream into the same response.
tsxexport default function ProductPage() { return ( <> <ProductInfo /> {/* cached - part of the shell */} <Suspense fallback={<CartSkeleton />}> <Cart /> {/* reads cookies() - streams in */} </Suspense> </> ); }How streaming works
- The server immediately sends the shell HTML with the
fallbackin place of each hole. - As soon as an async component resolves, React sends its HTML in the same response with a small inline script that swaps it into place.
- Holes resolve independently: a slow block doesn't delay the rest of the page.
Benefits
- Fast TTFB and FCP, as with a static page.
- Personalized data without making the whole route dynamic.
loading.tsxis just a<Suspense>boundary around the page created by convention.
Note:
<Suspense>itself does not make anything dynamic. Only access to request data or uncached I/O turns a component into a hole; synchronous code finishes during prerendering anyway.updated
did you know it?Proxy
proxy.ts(calledmiddleware.tsbefore Next.js 16) is a function that runs before a request reaches a route. It can redirect, rewrite, change headers or respond directly.ts// proxy.ts (project root or src/) import { NextResponse, type NextRequest } from 'next/server'; export function proxy(request: NextRequest) { if (!request.cookies.has('session')) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); } export const config = { matcher: ['/dashboard/:path*'], };Good use cases
- Redirects and rewrites based on the request (locale, A/B tests, old URLs).
- Setting headers (CSP, security headers) for a group of routes.
- Optimistic auth checks: no session cookie -> redirect to login.
What to avoid
- Slow data fetching: Proxy runs on every matched request, including prefetches.
- Being the only authorization layer: check access again where the data is read (Server Components, Server Actions, Route Handlers).
- For static redirects use
redirects()innext.config.tsinstead.
Details
- One
proxy.tsper project; split logic into modules and import them. - Since Next.js 16 it runs on the Node.js runtime by default.
fetchcache options (next.revalidate,next.tags) have no effect inside Proxy.
updated
did you know it?Built-in optimizations
next/link
- Client-side navigation: only the changed segments are rendered, shared layouts keep their state.
- Prefetching in production when a link enters the viewport: for static routes the whole route, for dynamic ones the shell down to the nearest
loading.tsx. prefetch={false}turns it off for rarely used links.
next/image
tsximport Image from 'next/image'; <Image src={hero} alt="Product" priority sizes="(min-width: 768px) 50vw, 100vw" />- Serves modern formats (WebP/AVIF) and resized versions for each screen via
srcset. - Requires
width/height(orfill) so the space is reserved and there is no layout shift (CLS). - Lazy loads images outside the viewport;
prioritypreloads the LCP image.
next/font
tsximport { Inter } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap' });- Fonts are downloaded at build time and self-hosted: no requests to Google from the browser.
- Generates a fallback font with adjusted metrics, so text doesn't jump when the font loads.
Summary
Component Improves next/linkNavigation speed (INP, perceived speed) next/imageLCP, CLS, traffic next/fontCLS, privacy, fewer requests updated
did you know it?Parallel routes
Slots (
@namefolders) render several pages in one layout at the same time. Each slot gets its own loading and error states and navigates independently.textapp/dashboard/ layout.tsx page.tsx @analytics/page.tsx @team/page.tsxtsxexport default function Layout({ children, analytics, team }: LayoutProps<'/dashboard'>) { return ( <> {children} <div className="grid grid-cols-2">{analytics}{team}</div> </> ); }When a slot has no page for the current URL after a full reload, Next.js renders its
default.tsx(or 404 without it).Intercepting routes
(.),(..),(...)folders show another route inside the current layout during client-side navigation, while a direct visit or reload opens the full page.Convention Intercepts (.)photoA segment on the same level (..)photoOne level up (...)photoFrom the root appTogether: a modal with its own URL
textapp/ @modal/(.)photo/[id]/page.tsx <- modal on top of the feed @modal/default.tsx <- returns null photo/[id]/page.tsx <- full page on reload layout.tsx <- renders {children}{modal}- Clicking a photo opens a modal and changes the URL, so it can be shared.
- Back closes the modal, reload shows the full page.
updated
did you know it?