Skip to content
masterclassBuddyTech Buzz5 fresh blogs · 5:00 AM IST

Technology handbook

Next.js 16+ APIs — Directives, Functions, Hooks & Config

130 entries covering the Next.js 16 surface: directives, components, file conventions, server functions, caching APIs, client hooks and every next.config key.

Topics
130
Sections
12

Directives

"use client"

Marks the module and its imports as the client boundary. Everything above it stays a Server Component.

  • It goes at the very top of the file, above imports; every module that file imports becomes client code too.
  • Props crossing into a Client Component must be serializable — functions cannot be passed, except Server Functions.
  • Push it down to the leaf that needs state or browser APIs, so the parent layout and its data stay on the server.

Example: app/posts/[id]/page.tsx

import { LikeButton } from "./like-button";
import { getPost } from "@/lib/data";

export default async function PostPage({ params }: PageProps<"/posts/[id]">) {
  const { id } = await params;
  const post = await getPost(id);
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.excerpt}</p>
      <LikeButton initialLikes={post.likes} />
    </article>
  );
}

An interactive like button on a server-rendered post: LikeButton.tsx starts with 'use client' and uses useState; the post page stays a Server Component and simply renders <LikeButton postId={id} />.

"use server"

Marks a module or function as a Server Function callable from the client. Treat its arguments as untrusted input.

  • At the top of a file it marks every export as a Server Function; inside an async function body it marks just that one.
  • Every Server Function is a public POST endpoint — check the session and validate input inside it, not in the page that calls it.
  • Arguments and return values must be serializable, and the call is queued: Server Functions from one client run one at a time.

Example: app/settings/actions.ts

"use server";
import { z } from "zod";
import { updateTag } from "next/cache";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";

const Profile = z.object({ name: z.string().min(1).max(80) });

export async function updateProfile(formData: FormData) {
  const session = await auth();
  if (!session) throw new Error("Not signed in");
  const { name } = Profile.parse({ name: formData.get("name") });
  await db.user.update({ where: { id: session.userId }, data: { name } });
  updateTag(`user-${session.userId}`);
}

Saving a profile form: async function updateProfile(formData) { 'use server'; const user = await auth(); … } passed to <form action={updateProfile}> works before JavaScript loads.

"use cache"

Caches a function, component or route by its inputs. The foundation of the Cache Components model.

  • Needs cacheComponents: true in next.config; the cache key is built from the function's arguments and any closed-over values.
  • It cannot read cookies(), headers() or searchParams inside the scope — read them outside and pass the values in as arguments.
  • Pair it with cacheLife('hours') for how long it lives and cacheTag('posts') so a mutation can invalidate it.

Example: next.config.ts

import type { NextConfig } from "next";

const nextConfig: NextConfig = { cacheComponents: true };
export default nextConfig;

A product list rendered once for every visitor: async function getProducts() { 'use cache'; cacheLife('hours'); cacheTag('products'); return db.product.findMany() } — the page's static shell includes the list.

"use cache: private"

Caches a function that reads cookies(), headers() or searchParams — in the browser's memory only, never on the server.

  • Use it only when moving cookies() or headers() outside the cached function and passing values in is not practical.
  • The function still runs on every server render; what is saved is repeat work during client navigation and prefetch.
  • Custom cache handlers cannot be used with it, and the stale time must be at least 30 seconds for per-link prefetching to work.

Personalised recommendations: A getRecommendations() that reads the session cookie is marked 'use cache: private' with cacheLife({ stale: 60 }), so moving between tabs reuses it without storing user data on the server.

"use cache: remote"

Caches into the shared remote cache handler rather than only in memory.

  • The default use cache store is in-memory per instance — evicted under pressure and lost on restart; remote is durable and shared.
  • Every lookup now costs a network round trip, so it only pays off for slow or rate-limited upstream calls.
  • Avoid it for keys that are unique per request (search filters, user ids) — the hit rate will be near zero.

A CMS with a strict rate limit: Ten server instances render the same article; 'use cache: remote' backed by a Redis cacheHandlers entry means the CMS is called once, not ten times.

Components

<Image>

Automatic sizing, lazy loading, responsive srcset and format negotiation. Needs width/height or fill.

  • Remote images need their host allowed in images.remotePatterns, or the optimizer refuses them.
  • Use sizes with fill or responsive layouts — without it the browser may download the largest variant.
  • Mark the hero image with preload (formerly priority) so the Largest Contentful Paint image is not lazy-loaded.

Example: next.config.ts

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [new URL("https://cdn.shop.example.com/products/**")],
    formats: ["image/avif", "image/webp"],
  },
};
export default nextConfig;

A responsive product hero: <Image src={p.photo} alt={p.name} fill sizes="(max-width: 768px) 100vw, 50vw" preload /> serves a small WebP/AVIF to phones and a larger one to desktops.

<Script>

Third-party scripts with an explicit strategy — beforeInteractive, afterInteractive, lazyOnload or worker.

  • afterInteractive (the default) loads after hydration; lazyOnload waits for browser idle time — right for chat widgets.
  • beforeInteractive must be placed in the root layout and is for scripts the whole site needs before hydration.
  • onLoad and onReady callbacks require a Client Component, because they run in the browser.

Loading analytics without slowing the page: <Script src="https://example.com/analytics.js" strategy="lazyOnload" /> keeps a third-party tag off the critical path.

<Form>

A form that navigates or invokes a Server Function with progressive enhancement and prefetching.

  • With a string action it does a GET navigation that writes the inputs to the URL's search params — ideal for search boxes.
  • It prefetches the destination's loading UI when the form becomes visible, so submitting feels instant.
  • With a function action it behaves like a React form that calls a Server Function.

A search box that updates the URL: <Form action="/search"><input name="q" /></Form> navigates to /search?q=shoes client-side, and the results page reads searchParams.q.

next/font

Self-hosts Google and local fonts at build time — no network request, and no layout shift.

  • Fonts are downloaded at build time and served from your own domain, so no request goes to Google at runtime.
  • Call the loader at module scope, then apply font.className or expose it as a CSS variable with variable.
  • Variable fonts are recommended; for others you must list the weight values you use.

Example: app/fonts.ts

import { Inter } from "next/font/google";
import localFont from "next/font/local";

export const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
export const brand = localFont({ src: "./brand.woff2", variable: "--font-brand" });

One brand font across the app: const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }) in the root layout, then <html className={inter.variable}>.

File conventions

layout.js

Shared UI that wraps a segment and its children, and does not remount on navigation within it.

  • The root layout is required and must render <html> and <body>.
  • A layout receives children and its params, but not searchParams — it is not re-rendered when only the query changes.
  • Because it does not remount on navigation, state inside it (an open sidebar, a playing video) survives page changes.

A dashboard shell: app/dashboard/layout.tsx renders the sidebar and {children}; moving from /dashboard/a to /dashboard/b re-renders only the page.

page.js

Makes a route segment publicly routable and renders its UI.

  • params and searchParams are promises in Next.js 16 — const { id } = await params.
  • A page is a Server Component by default, so it can await the database directly.
  • PageProps<'/blog/[slug]'> gives fully typed props generated from your route tree.

A blog post page: app/blog/[slug]/page.tsx awaits params, loads the post and calls notFound() when it does not exist.

template.js

Like a layout but remounts on every navigation, so state and effects reset.

  • A template is given a unique key per route, so navigating between pages remounts it and its children.
  • Use it when you need effects to rerun or state to reset per page — enter animations, per-page analytics events.
  • It renders between the layout and the page; prefer a layout unless the remount is the point.

A page-enter animation: app/template.tsx wraps children in an animated <div> so every navigation replays the fade-in that a layout would only play once.

loading.js

An automatic Suspense boundary for the segment and everything under it.

  • It wraps the page in <Suspense>, so the layout stays interactive while the page's data streams in.
  • The fallback is prefetched with the route, so a click shows it immediately.
  • For finer control, place your own <Suspense> boundaries inside the page around slow parts only.

Example: app/orders/loading.tsx

export default function Loading() {
  return (
    <table aria-busy="true">
      <tbody>
        {Array.from({ length: 5 }, (_, i) => <tr key={i}><td className="skeleton" /></tr>)}
      </tbody>
    </table>
  );
}

A skeleton while orders load: app/orders/loading.tsx returns a skeleton table; the header and nav render at once and the real table streams in when the query finishes.

error.js

A client error boundary for the segment, receiving error and reset.

  • It must be a Client Component ('use client'), because error boundaries are a client feature.
  • It catches errors in its segment's children, not in the layout of the same segment — use the parent's error.js for that.
  • global-error.js covers the root layout and must render its own <html> and <body>.

Example: app/dashboard/error.tsx

"use client";

export default function DashboardError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
  return (
    <div role="alert">
      <h2>We couldn't load your dashboard.</h2>
      <p>Reference: {error.digest}</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Recovering from a failed widget: app/dashboard/error.tsx shows "Something went wrong" with a Try again button calling reset(), while the rest of the app keeps working.

not-found.js

The UI rendered by notFound() or by an unmatched route.

  • app/not-found.js handles every unmatched URL in the app, not just calls to notFound().
  • A not-found response is returned with HTTP 404 and noindex so search engines drop it.
  • A segment-level not-found.js lets each section show its own message, such as "Product not found" with a search box.

A missing product: const product = await getProduct(id); if (!product) notFound(); renders app/products/[id]/not-found.tsx.

forbidden.js

The UI for forbidden() — a 403, distinct from a 404. Requires authInterrupts.

  • Enable it with experimental: { authInterrupts: true } in next.config.
  • Use it for a signed-in user who lacks permission; signed-out users belong to unauthorized.js.
  • The page responds with HTTP 403, so clients and crawlers can tell it apart from a missing page.

An admin-only page: if (session.role !== 'admin') forbidden(); shows app/forbidden.tsx with "You don't have access" instead of a confusing 404.

unauthorized.js

The UI for unauthorized() — a 401, for signed-out rather than under-privileged.

  • Also needs experimental.authInterrupts; it renders with HTTP 401.
  • It is the natural place for a sign-in form or a link to /sign-in?next=….
  • Calling unauthorized() in a layout protects every page below it with one check.

Private account settings: const session = await auth(); if (!session) unauthorized(); renders a sign-in prompt in place of the settings page.

route.js

A Route Handler exporting GET, POST and friends, built on Request and Response.

  • Export a function per HTTP method; it receives a Request (or NextRequest) and returns a Response.
  • A route.js cannot sit in the same folder as a page.js — one segment is either a page or an endpoint.
  • GET handlers are dynamic by default; under Cache Components put 'use cache' on a helper to cache the work.

Example: app/api/webhooks/payments/route.ts

import { createHmac, timingSafeEqual } from "node:crypto";

export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get("x-signature") ?? "";
  const expected = createHmac("sha256", process.env.WEBHOOK_SECRET!).update(body).digest("hex");

  const valid = signature.length === expected.length
    && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  if (!valid) return new Response("Invalid signature", { status: 401 });

  const event = JSON.parse(body);
  if (event.type === "payment.captured") {
    // await markOrderPaid(event.data.orderId);
  }
  return new Response(null, { status: 204 });
}

A webhook receiver: export async function POST(req: Request) { const body = await req.text(); verifySignature(body, req.headers); … return new Response(null, { status: 204 }) }.

proxy.js

Runs before a request is matched — rewrites, redirects and headers. The successor to middleware.js.

  • It lives at the project root (or src/) and exports a proxy function plus an optional config.matcher.
  • It runs on the Node.js runtime; the runtime option cannot be set in it.
  • Keep it to fast checks — redirects, rewrites, headers — not full session validation or database work.

Example: proxy.ts

import { NextResponse, type NextRequest } from "next/server";

export function proxy(request: NextRequest) {
  if (!request.cookies.has("session")) {
    const signIn = new URL("/sign-in", request.url);
    signIn.searchParams.set("next", request.nextUrl.pathname);
    return NextResponse.redirect(signIn);
  }
  return NextResponse.next();
}

export const config = { matcher: ["/dashboard/:path*", "/settings/:path*"] };

Redirecting signed-out visitors: export function proxy(req) { if (!req.cookies.has('session')) return NextResponse.redirect(new URL('/sign-in', req.url)) } with matcher: ['/dashboard/:path*'].

middleware.js

The earlier name for the same interception point; prefer proxy.js on new code.

  • Next.js 16 deprecates middleware.js and renames the convention to proxy.js.
  • Migrating means renaming the file and the exported function to proxy; a codemod does both.
  • The rename reflects its job: sitting in front of the app as a network proxy, not general-purpose middleware.

Upgrading a Next.js 15 app: Run npx @next/codemod@canary middleware-to-proxy . and the existing middleware.ts becomes proxy.ts with the same matcher.

default.js

The fallback a parallel route slot renders when it has no match for the current URL.

  • When a hard navigation (refresh) cannot recover a slot's active state, Next.js renders its default.js.
  • Without it an unmatched slot renders a 404 for the whole page.
  • Returning null from default.js is the usual way to say "show nothing here".

A modal slot that survives refresh: app/@modal/default.tsx returns null, so refreshing /photos does not 404 just because no modal is open.

instrumentation.js

register() runs once on server startup — where tracing and monitoring are initialised.

  • register() runs once when a new server instance starts, before it handles requests.
  • onRequestError receives every server error with route context — the hook for error-reporting services.
  • Import Node-only SDKs conditionally on process.env.NEXT_RUNTIME === 'nodejs'.

Wiring up OpenTelemetry: export function register() { registerOTel({ serviceName: 'shop' }) } sends traces for every route without touching page code.

instrumentation-client.js

The same idea in the browser, running before any application code.

  • It runs after the HTML loads and before React hydrates, so it can capture early errors.
  • Keep it small — anything slow here delays the page becoming interactive.
  • It can export onRouterTransitionStart to track client-side navigations.

Client error monitoring: instrumentation-client.ts initialises the error-tracking SDK and records a navigation span each time the router starts a transition.

mdx-components.js

Maps MDX elements to your components, required by @next/mdx.

  • It sits at the project root and exports useMDXComponents, which @next/mdx requires in the App Router.
  • Mapping h1 or a here styles every MDX page at once, without imports in each file.
  • Add md/mdx to pageExtensions so .mdx files can be pages.

A docs site written in MDX: useMDXComponents maps pre to a syntax-highlighting <CodeBlock> and a to <Link>, so authors write plain Markdown.

Dynamic routes

[slug], [...slug] catch-all and [[...slug]] optional catch-all segments.

  • [slug] matches one segment, [...slug] one or more, and [[...slug]] also matches the parent path itself.
  • The value arrives in params as a string, or a string array for catch-alls.
  • Pair them with generateStaticParams to prerender known values at build time.

Example: app/products/[id]/page.tsx

import { notFound } from "next/navigation";
import { getProduct } from "@/lib/products";

export default async function ProductPage({ params }: PageProps<"/products/[id]">) {
  const { id } = await params;
  const product = await getProduct(id);
  if (!product) notFound();
  return <h1>{product.name} — ₹{product.price}</h1>;
}

A documentation tree: app/docs/[...slug]/page.tsx serves /docs/a, /docs/a/b and deeper, with params.slug as ['a', 'b'].

Route groups

(group) folders organise files and share layouts without adding a URL segment.

  • Two groups can each define a root layout — for example a marketing site and an app with different <html> shells.
  • Two groups must not resolve to the same URL, or the build fails.
  • Navigating between different root layouts triggers a full page load.

Splitting marketing and app: app/(marketing)/page.tsx and app/(app)/dashboard/page.tsx share no layout, yet the URLs stay / and /dashboard.

Parallel routes

@slot folders render several pages into one layout at the same time.

  • Each @slot arrives as a prop on the parent layout, next to children.
  • Slots navigate independently, each with its own loading.js and error.js.
  • Every slot needs a default.js to stay visible after a refresh.

A dashboard with independent panels: @analytics and @team render side by side in layout.tsx; a slow analytics query streams in without holding up the team panel.

Intercepting routes

(.), (..) and (...) render another route in place — the pattern behind modal navigation.

  • (.) matches the same level, (..) one level up and (...) the app root — by route segments, not file folders.
  • Interception only happens on client navigation; a refresh or a shared link loads the real page.
  • It is usually combined with a parallel @modal slot.

Example: app/@modal/(.)photos/[id]/page.tsx

import { Modal } from "@/components/modal";
import { getPhoto } from "@/lib/photos";

export default async function PhotoModal({ params }: PageProps<"/photos/[id]">) {
  const photo = await getPhoto((await params).id);
  return <Modal><img src={photo.url} alt={photo.alt} /></Modal>;
}

Instagram-style photo modal: Clicking a thumbnail renders app/@modal/(.)photo/[id]/page.tsx as a modal over the feed; opening the URL directly shows the full photo page.

src folder

An optional src/ root keeping application code apart from config at the repository root.

  • When src/ is used, app/ and pages/ move inside it, and config files stay at the root.
  • proxy.ts and instrumentation.ts go inside src/ as well.
  • public/ stays at the root — it is not moved into src/.

Keeping a monorepo package tidy: Root holds next.config.ts, package.json and public/; all application code lives under src/app.

public folder

Served at the root as-is. Anything here is public, so never put secrets in it.

  • A file at public/logo.png is served at /logo.png; the folder name is not part of the URL.
  • Files here are not cached aggressively by default because they can change between deploys under the same name.
  • Only files present at build time are served — files added at runtime are not.

Site-wide static assets: public/robots.txt, public/favicon.ico and public/og-default.png are served as-is with no import needed.

Metadata files

generateMetadata

Async per-route metadata, with parent metadata available for merging.

  • It can await the same data as the page; fetch calls are deduped, so the data is fetched once.
  • It receives the resolved parent metadata as a second argument for extending, e.g., Open Graph images.
  • For most crawlers metadata streams after the page starts; the bots in htmlLimitedBots receive it blocking.

Example: app/products/[id]/page.tsx

import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getProduct } from "@/lib/products";

export async function generateMetadata({ params }: PageProps<"/products/[id]">): Promise<Metadata> {
  const product = await getProduct((await params).id);
  return {
    title: product?.name ?? "Product not found",
    description: product?.summary,
    openGraph: { images: product ? [product.image] : [] },
  };
}

export default async function ProductPage({ params }: PageProps<"/products/[id]">) {
  const product = await getProduct((await params).id);
  if (!product) notFound();
  return <h1>{product.name}</h1>;
}

SEO for each product page: export async function generateMetadata({ params }) { const p = await getProduct((await params).id); return { title: p.name, description: p.summary } }.

Metadata object

Static export for titles, descriptions, Open Graph, Twitter cards and robots.

  • Export metadata from a layout or page; child segments merge over their parents.
  • title: { template: '%s | Shop', default: 'Shop' } in the root layout gives every page a consistent suffix.
  • It cannot be exported from a Client Component, and a segment cannot export both metadata and generateMetadata.

Consistent page titles: The root layout sets the template; app/about/page.tsx exports { title: 'About' } and the tab reads "About | Shop".

generateViewport

Viewport and theme colour, kept separate from metadata so it is not recomputed.

  • viewport and themeColor moved out of metadata — set them with export const viewport or generateViewport.
  • themeColor can be an array with media queries for light and dark mode.
  • Use the function form only when the value depends on params or data.

Matching the browser bar to the theme: export const viewport = { themeColor: [{ media: '(prefers-color-scheme: dark)', color: '#000' }, { color: '#fff' }] }.

opengraph-image

A static image or a generated one; the .js form renders with ImageResponse at build or request time.

  • Drop an opengraph-image.png into a segment and Next.js adds the og:image tags automatically.
  • The .tsx form returns an ImageResponse, so each post can get a generated card with its title.
  • Export alt, size and contentType alongside it to describe the image.

Share cards for every blog post: app/blog/[slug]/opengraph-image.tsx renders the post title on a branded background, so links pasted into Slack show a real preview.

app icons

favicon, icon and apple-icon conventions, including generated variants.

  • Place icon.png or apple-icon.png in app/ and the <link> tags are generated for you.
  • favicon.ico only works at the top level of app/.
  • icon.tsx can generate an icon with ImageResponse, such as a letter or status badge.

Brand icons without manual tags: app/icon.svg and app/apple-icon.png replace the hand-written <link rel="icon"> lines from the old <Head>.

sitemap.js

Returns sitemap entries; generateSitemaps splits very large sites into several files.

  • Return an array of { url, lastModified, changeFrequency, priority }, typed as MetadataRoute.Sitemap.
  • It is a special Route Handler and is cached by default unless it uses request-time APIs.
  • One sitemap can hold at most 50,000 URLs — split larger sites with generateSitemaps.

Indexing every product: app/sitemap.ts loads product slugs from the database and returns their URLs with lastModified: p.updatedAt.

robots.js

Generates robots.txt from a typed object rather than a hand-edited file.

  • Return { rules, sitemap, host } typed as MetadataRoute.Robots.
  • Use it to change rules per environment — block all crawlers on preview deployments.
  • Rules can target user agents, e.g. disallow /admin for everyone.

Keeping staging out of Google: rules: process.env.VERCEL_ENV === 'production' ? { userAgent: '*', allow: '/' } : { userAgent: '*', disallow: '/' }.

manifest.js

Generates the web app manifest for installable PWAs.

  • Return a MetadataRoute.Manifest with name, start_url, display and icons.
  • Next.js serves it at /manifest.webmanifest and links it from every page.
  • Combined with a service worker it makes the app installable on phones and desktops.

An installable app: display: 'standalone' and 192px/512px icons let users add the app to their home screen.

Route segment config

dynamicParams

Whether params outside generateStaticParams render on demand or 404.

  • true (default): params not listed by generateStaticParams render on first request and are then cached.
  • false: unknown params return 404 — useful when the list is complete.
  • It is not available when Cache Components is enabled.

A closed list of locales: export const dynamicParams = false with generateStaticParams returning en and fr means /de returns 404 instead of rendering.

revalidate

Segment-wide revalidation period in the previous caching model. Removed under Cache Components — use cacheLife instead.

  • In the previous model, export const revalidate = 3600 set a page's ISR interval in seconds.
  • With cacheComponents: true the export errors; move the timing to cacheLife() inside 'use cache'.
  • The closest built-in profile works for most values: 'minutes', 'hours', 'days', 'weeks' or 'max'.

Migrating an hourly blog index: export const revalidate = 3600 becomes 'use cache'; cacheLife('hours') at the top of the data function.

runtime

nodejs (the default) or edge for the segment. The Edge runtime is deprecated in 16 — remove the export.

  • Cache Components requires the Node.js runtime, so runtime = 'edge' blocks adopting it.
  • The option cannot be used in proxy.js.
  • Fluid compute on Node removes most of the cold-start reason for choosing Edge.

Removing an old edge flag: Delete export const runtime = 'edge' from app/api/og/route.ts; it runs on Node with the full Node API available.

maxDuration

The execution budget for the segment on platforms that enforce one.

  • Set in seconds: export const maxDuration = 60.
  • The platform's plan limit still applies; the value can only ask for time within it.
  • Use it for long-running routes such as AI streaming or report generation.

A long AI response: app/api/chat/route.ts exports maxDuration = 300 so a streamed answer is not cut off at the default limit.

preferredRegion

Deployment hint for placing the segment near its data. Deprecated in 16; set regions on the platform instead.

  • Values are 'auto', 'global', 'home' or specific region ids.
  • It is a hint the hosting platform may honour, not a guarantee.
  • Placing compute next to the database usually matters more than next to the user.

A database in one region: Instead of per-route hints, configure the function region in the platform to match the database's region.

prefetch

Caps how much of a segment is prefetched: 'partial' for its App Shell, 'force-disabled' for none. Requires Cache Components.

  • 'auto' is the default — do not write it explicitly.
  • 'partial' lets you adopt Partial Prefetching route by route before turning on partialPrefetching app-wide.
  • <Link prefetch={false}> still wins at the link level, whatever the segment says.

Rarely visited settings pages: export const prefetch = 'force-disabled' on app/settings/layout.tsx stops links all over the app from prefetching pages users seldom open.

instant

Asks Next.js to validate, in development, that navigating into this segment shows UI immediately. Requires Cache Components.

  • Validation runs in development and shows the component that would block navigation in the dev overlay.
  • The usual fix is to cache the data with 'use cache' or wrap the slow part in <Suspense>.
  • instant = false on a layout allows that layout to block while pages below it are still validated.

Proving tab switches are instant: export const instant = true on app/tabs/[tab]/page.tsx flags an uncached fetch that would freeze the screen between tabs.

Server functions

Server Functions

'use server' functions invoked from the client. Always re-check authorisation inside them.

  • Used as a form action they work before JavaScript loads (progressive enhancement).
  • From a Client Component they can be imported from a 'use server' file and called like an async function.
  • After a mutation, call updateTag, revalidatePath or refresh so the UI shows the new data.

Example: app/editor/actions.ts

"use server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";

export async function saveDraft(postId: string, body: string) {
  const session = await auth();
  const post = await db.post.findUnique({ where: { id: postId } });
  if (!session || post?.authorId !== session.userId) throw new Error("Not allowed");
  await db.post.update({ where: { id: postId }, data: { body, savedAt: new Date() } });
  return { savedAt: new Date().toISOString() };
}

Creating a todo: <form action={createTodo}> posts to createTodo(formData), which checks the session, inserts the row and calls updateTag('todos').

revalidatePath

Invalidates the cache for a path after a mutation.

  • Pass a path like /blog, or a route pattern with 'page' or 'layout' as the second argument for dynamic segments.
  • revalidatePath('/', 'layout') clears everything — powerful, rarely what you want.
  • Prefer tags when the same data appears on many paths.

Refreshing the blog index after publishing: A publishPost Server Function calls revalidatePath('/blog') so the new post appears in the list.

revalidateTag

Marks every entry with a tag stale; pass "max" as the second argument for stale-while-revalidate. Works in Route Handlers too.

  • A revalidation is triggered by the next request for that data, so pages update as they are visited, not all at once.
  • Without a second argument it is deprecated and behaves like { expire: 0 } — a blocking cache miss.
  • Tags are case-sensitive and limited to 256 characters.

Example: lib/posts.ts

import { cacheLife, cacheTag } from "next/cache";

export async function getPosts() {
  "use cache";
  cacheLife("days");
  cacheTag("posts");
  const res = await fetch("https://cms.example.com/api/posts");
  return res.json();
}

A CMS webhook: app/api/revalidate/route.ts verifies the CMS signature, then calls revalidateTag('posts', 'max') so readers get fresh content in the background.

updateTag

Invalidates a tag and refreshes in the same pass, so the caller sees fresh data immediately.

  • It only works inside Server Actions; Route Handlers use revalidateTag.
  • It expires the data immediately, so the next request waits for fresh data instead of seeing stale content.
  • Designed for read-your-own-writes: the user who made the change must see it straight away.

Editing a post: async function savePost(f) { 'use server'; await db.post.update(…); updateTag(post-${id}) } — the author sees the edit on the very next render.

refresh

Refreshes the client router from inside a Server Action, so the page re-renders with fresh server data. Server Actions only.

  • It cannot be called from Route Handlers or Client Components — calling it there throws.
  • Use it when the changed data is uncached, so there is no tag or path to invalidate.
  • It takes no arguments and returns nothing.

Updating a live counter: A vote Server Action writes the vote and calls refresh(), so the uncached vote total re-renders without a page reload.

redirect

Throws to redirect. It must not be called inside a try that swallows the thrown control signal.

  • It works in Server Components, Server Functions and Route Handlers; in a Server Function it returns 303.
  • Call it after the try/catch, or rethrow with unstable_rethrow, because it works by throwing.
  • Pass 'replace' or 'push' as the second argument to control the browser history entry.

Going to the new post after creating it: const post = await createPost(data); redirect(/posts/${post.id}) — placed after the try block that handles database errors.

permanentRedirect

The 308 counterpart, for URLs that have moved for good.

  • It returns 308, which tells browsers and search engines to update their links.
  • Use it when a URL has moved for good, such as a renamed slug.
  • For many fixed redirects, prefer redirects in next.config, which runs before rendering.

Renamed product slugs: if (product.slug !== slug) permanentRedirect(/p/${product.slug}) keeps old links working and passes their search ranking on.

notFound

Throws to render the nearest not-found.js.

  • It returns HTTP 404 and adds noindex to the page.
  • Its return type is never, so TypeScript narrows the value as non-null after the call.
  • Call it before any streamed content is sent so the status code is still correct.

Unknown user profiles: const user = await getUser(handle); if (!user) notFound(); then user.name is safely typed.

forbidden

Throws a 403 to render forbidden.js. Requires the authInterrupts flag.

  • It can be called in Server Components, Server Functions and Route Handlers.
  • It cannot be called in the root layout.
  • Check permissions close to the data, not only in proxy.js.

Role checks on an admin page: const session = await verifySession(); if (session.role !== 'admin') forbidden();.

unauthorized

Throws a 401 to render unauthorized.js.

  • It requires experimental.authInterrupts and renders unauthorized.js with a 401.
  • It cannot be called in the root layout.
  • Use it for missing sign-in; use forbidden() when the user is signed in but not allowed.

Protecting a dashboard: const session = await auth(); if (!session) unauthorized(); at the top of app/dashboard/page.tsx.

after

Schedules work to run after the response is sent — logging and analytics without delaying the user.

  • It runs after the response finishes streaming, even when the response failed or redirected.
  • Inside after you can still read cookies() and headers() in Server Functions and Route Handlers.
  • It is not a durable queue — use a job system for work that must survive a crash.

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

import { after } from "next/server";
import { getPost } from "@/lib/posts";
import { trackView } from "@/lib/analytics";

export default async function Post({ params }: PageProps<"/blog/[slug]">) {
  const { slug } = await params;
  const post = await getPost(slug);
  after(() => trackView(slug));
  return <article><h1>{post.title}</h1></article>;
}

Logging a page view without slowing the page: after(() => analytics.track('view', { path })) records the view once the user already has the HTML.

connection

Marks the point where rendering must wait for a real request, opting out of prerendering.

  • Use it before Math.random(), new Date() or a synchronous database query that must run per request.
  • Unlike io(), it also blocks prefetches until a real navigation reaches the server.
  • With cookies() or headers() already in use, it is not needed — they are request signals themselves.

A per-visit random banner: await connection(); const tip = tips[Math.floor(Math.random() * tips.length)] gives each request its own tip instead of one baked into the build.

unstable_rethrow

Re-throws Next's internal control-flow errors so a broad catch does not swallow a redirect.

  • It rethrows only Next.js internal errors and does nothing for your own errors.
  • Call it first thing in the catch block.
  • It covers notFound, redirect, permanentRedirect and request-time API bailouts.

A `try` that wraps a redirect: try { … redirect('/done') } catch (err) { unstable_rethrow(err); log(err) } — the redirect still happens.

catchError

Creates an error-boundary component in code — the programmatic form of error.js that lets redirect() and notFound() pass through.

  • The fallback receives your props plus { error, retry } (or reset outside the App Router).
  • retry() re-renders inside a Transition, keeping Client Component state outside the boundary.
  • The error state clears automatically on client navigation to another route.

Isolating a flaky widget: const SafeChart = catchError(ChartError) wraps one chart so a failing data feed shows "Try again" without replacing the whole page.

Caching APIs

cacheLife

Sets the stale, revalidate and expire profile for a 'use cache' boundary.

  • Built-in profiles: 'seconds', 'minutes', 'hours', 'days', 'weeks' and 'max'.
  • stale is how long the client uses it without asking, revalidate when the server refreshes it in the background, expire when it must be refetched.
  • Define your own named profiles in next.config's cacheLife key.

A dashboard metric refreshed every minute: async function getStats() { 'use cache'; cacheLife('minutes'); return queryStats() }.

cacheTag

Tags a cached entry so revalidateTag can invalidate it later.

  • Call it inside a 'use cache' scope; one entry can carry several tags.
  • Tag by entity — cacheTag('products', product-${id}) — so you can clear one item or all of them.
  • Tags are invalidated with updateTag or revalidateTag.

Clearing one product after an edit: The product loader tags product-42; the edit action calls updateTag('product-42') and the other products stay cached.

cacheComponents

The config flag enabling the Cache Components model, where caching is explicit rather than inferred.

  • With it on, nothing is cached unless you opt in with 'use cache'.
  • It makes Partial Prerendering the default: a static shell is sent first and dynamic parts stream in.
  • Segment options dynamic, revalidate and fetchCache are removed when it is on.

Adopting the Next.js 16 caching model: const nextConfig = { cacheComponents: true }, then add 'use cache' to the data functions that can be shared.

cacheHandlers

Pluggable cache backends, so the cache can be shared across instances.

  • The default in-memory cache is per process and lost on restart.
  • Configure a remote handler (Redis, DynamoDB…) to share entries across instances.
  • Most apps on a managed host do not need one — the platform provides it.

Self-hosting on several containers: cacheHandlers: { remote: require.resolve('./cache-redis.js') } lets all containers share 'use cache: remote' entries.

unstable_cache

The previous-generation data cache wrapper. 'use cache' supersedes it.

  • It wraps an async function with key parts, tags and revalidate options.
  • Migrate to 'use cache' with cacheTag and cacheLife when moving to Cache Components.
  • It cannot call cookies() or headers() inside the cached function.

Migrating a cached query: unstable_cache(getUser, ['user'], { tags: ['users'] }) becomes a function with 'use cache'; cacheTag('users').

unstable_noStore

Opts a call out of caching. connection() is the current way to express this.

  • It is the old way to opt a component out of static rendering.
  • Today, call await connection() instead.
  • With Cache Components everything is dynamic by default, so most uses disappear.

Cleaning up legacy code: Replace noStore() at the top of a page with await connection().

fetch options

cache and next: { revalidate, tags } control caching per request.

  • cache: 'force-cache' caches the response; 'no-store' fetches every time.
  • next: { revalidate: 60 } refreshes at most once a minute; next: { tags: ['posts'] } allows targeted invalidation.
  • Under Cache Components, put the fetch in a 'use cache' function and use cacheLife/cacheTag instead.

Caching a third-party API: fetch(url, { next: { revalidate: 3600, tags: ['rates'] } }) limits a currency API to one call per hour.

staleTimes

Experimental: how long the client router reuses a cached page segment (static / dynamic seconds) before refetching.

  • Set under experimental: { staleTimes: { dynamic: 30, static: 180 } }.
  • static applies to static pages or prefetch={true} links; dynamic to the rest.
  • Shared layouts are not refetched on navigation anyway — only the changed page segment is.

Smoother back-and-forth navigation: staleTimes.dynamic: 30 lets users switch between two dynamic pages for 30 seconds without a refetch.

expireTime

The stale-while-revalidate window sent to a CDN for ISR responses.

  • It sets the stale-while-revalidate value in the Cache-Control header for ISR pages.
  • The default is one year.
  • It matters when a CDN in front of Next.js caches pages.

CDN-cached marketing pages: expireTime: 3600 stops a CDN serving a page older than an hour while it revalidates.

Request APIs

cookies

Async reader and writer for request cookies. Writing is only allowed in a Server Function or Route Handler.

  • It is async: const store = await cookies(); store.get('theme').
  • set and delete are only allowed in Server Functions and Route Handlers, because headers are already sent while rendering.
  • Reading cookies makes the component request-time, so it is not part of the static shell.

Example: app/actions.ts

"use server";
import { cookies } from "next/headers";

export async function setTheme(theme: "light" | "dark") {
  (await cookies()).set("theme", theme, { path: "/", maxAge: 60 * 60 * 24 * 365 });
}

Remembering a theme: A setTheme Server Function calls (await cookies()).set('theme', 'dark'), and the layout reads it to set <html className>.

headers()

Async read-only access to request headers. Awaiting it makes the route dynamic.

  • It returns a read-only Headers object.
  • Use it for user-agent, accept-language or a forwarded IP set by a proxy.
  • Wrap the component that reads it in <Suspense> so the rest of the page can still prerender.

Choosing a default language: const lang = (await headers()).get('accept-language')?.split(',')[0] ?? 'en'.

draftMode

Enables and disables preview rendering for CMS drafts.

  • (await draftMode()).enable() sets a bypass cookie; disable() removes it.
  • When enabled, pages render at request time so unpublished content is visible.
  • Protect the enabling route with a secret so only editors can switch it on.

Previewing CMS drafts: app/api/draft/route.ts checks ?secret=…, enables draft mode and redirects to the article, which then fetches draft content.

userAgent

Parses the UA header into device, browser and bot fields.

  • Import it from next/server and pass it a request: userAgent(req).
  • It returns isBot, browser, device, engine, os and cpu.
  • Common in proxy.js for device-based rewrites.

Mobile-specific landing page: In proxy.ts, if (userAgent(req).device.type === 'mobile') return NextResponse.rewrite(new URL('/m', req.url)).

NextRequest

Extends Request with cookies, nextUrl and geo/IP where the platform provides it.

  • req.nextUrl is a URL object with pathname and searchParams, plus Next.js extras like basePath.
  • req.cookies.get/has/set/delete read and change request cookies.
  • It is accepted wherever a Request is, so you can type Route Handler arguments with it.

Reading a query param in a Route Handler: export function GET(req: NextRequest) { const q = req.nextUrl.searchParams.get('q') }.

NextResponse

Extends Response with redirect, rewrite, next and cookie helpers, mainly for proxy.js.

  • NextResponse.next() continues the request — used in proxy to add headers.
  • NextResponse.rewrite(url) serves another path while keeping the URL in the address bar.
  • NextResponse.json(data, { status }) is a shortcut for JSON responses in Route Handlers.

Adding a security header in proxy: const res = NextResponse.next(); res.headers.set('x-frame-options', 'DENY'); return res.

ImageResponse

Renders JSX to an image at the edge — the engine behind dynamic Open Graph images.

  • Import it from next/og; it supports flexbox and a subset of CSS, not grid.
  • Fonts must be loaded as ArrayBuffers and passed in fonts.
  • The bundle is limited to 500KB, including fonts and images.

Dynamic social images: new ImageResponse(<div style={{ display: 'flex', fontSize: 64 }}>{title}</div>, { width: 1200, height: 630 }).

generateStaticParams

Enumerates the dynamic params to prerender at build time.

  • Return an array of param objects, e.g. [{ slug: 'a' }, { slug: 'b' }].
  • Returning a partial list prerenders the popular pages and leaves the rest to render on demand.
  • Under Cache Components at least one param is required so the route can be validated at build.

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

import { getTopPosts, getPost } from "@/lib/posts";

export async function generateStaticParams() {
  const posts = await getTopPosts(100);
  return posts.map(post => ({ slug: post.slug }));
}

export default async function Post({ params }: PageProps<"/blog/[slug]">) {
  const post = await getPost((await params).slug);
  return <article><h1>{post.title}</h1></article>;
}

Prerendering the top 100 posts: return (await getTopPosts(100)).map(p => ({ slug: p.slug })) — older posts render on first request.

generateImageMetadata

Generates several images from one file convention, each with its own metadata.

  • Return an array of { id, alt, size, contentType }, one per image.
  • The image function receives each id to render the matching variant.
  • Useful for several icon sizes or several Open Graph images from one file.

Icons in two sizes: generateImageMetadata returns small (48px) and large (72px) entries, and icon.tsx renders each size.

rootParams

Reads the root layout's dynamic params, for multi-tenant and internationalised roots.

  • Import a getter named after the segment, e.g. import { locale } from 'next/root-params', then await locale().
  • Works in Server Components only — not in Client Components, Server Actions or Route Handlers yet.
  • The segment name must be a valid JavaScript identifier, so [post-slug] is not supported.

Translations without prop drilling: A deep <Price> component calls await locale() to format currency, without the page passing locale down five levels.

io

Marks a call as real I/O, so the prerenderer knows it cannot be evaluated at build time.

  • Use await io() in Server Components and use(io()) in Client Components before reading the clock or random values.
  • Outside prerendering — real requests, cached scopes, the browser — it resolves immediately.
  • Unlike connection(), it does not block prefetches.

Showing the current time: async function Now() { await io(); return <p>{new Date().toLocaleTimeString()}</p> } inside <Suspense> keeps a build-time timestamp out of the shell.

Client hooks

useRouter

push, replace, refresh, back and forward from a Client Component. Import from next/navigation.

  • Import it from next/navigation, not next/router (the Pages Router one).
  • router.refresh() re-fetches Server Components without losing client state.
  • Prefer <Link> for navigation; use the router for navigation after an event, like a successful save.

Example: components/new-project-form.tsx

"use client";
import { useRouter } from "next/navigation";
import { createProject } from "@/app/projects/actions";

export function NewProjectForm() {
  const router = useRouter();
  async function onSubmit(formData: FormData) {
    const project = await createProject(formData);
    router.push(`/projects/${project.id}`);
  }
  return <form action={onSubmit}><input name="name" /><button>Create</button></form>;
}

Navigating after a form succeeds: const router = useRouter(); … await save(); router.push('/orders').

usePathname

The current path, for active-link styling and analytics.

  • It is a Client Component hook and returns the path without the query string.
  • With rewrites it returns the URL the user sees, not the internal route.
  • Pair it with useSearchParams when you need the query too.

Example: components/nav-link.tsx

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";

export function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
  const pathname = usePathname();
  const active = pathname === href || pathname.startsWith(`${href}/`);
  return <Link href={href} aria-current={active ? "page" : undefined} className={active ? "active" : ""}>{children}</Link>;
}

Highlighting the active nav item: const pathname = usePathname(); <Link className={pathname === href ? 'active' : ''} href={href}>.

useSearchParams

A read-only URLSearchParams. Wrap the component in Suspense or the whole route opts out of static rendering.

  • The returned object is read-only — to change the query, build a new string and router.push it.
  • Pages can read the same data on the server through the searchParams prop.
  • In layouts it is not updated by the server, which is why it is a client hook.

Example: components/sort-select.tsx

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

export function SortSelect() {
  const searchParams = useSearchParams();
  const router = useRouter();
  const pathname = usePathname();

  function onChange(sort: string) {
    const params = new URLSearchParams(searchParams);
    params.set("sort", sort);
    router.replace(`${pathname}?${params}`);
  }

  return (
    <select value={searchParams.get("sort") ?? "newest"} onChange={e => onChange(e.target.value)}>
      <option value="newest">Newest</option>
      <option value="price">Price</option>
    </select>
  );
}

A filter that writes to the URL: const params = new URLSearchParams(searchParams); params.set('sort', 'price'); router.replace(?${params}).

useParams

The current route's dynamic parameters as an object.

  • Values are strings, or string arrays for catch-all segments.
  • It returns params for the whole active route, including ones above the calling component.
  • Server Components get the same values from the params prop instead.

Example: components/review-form.tsx

"use client";
import { useParams } from "next/navigation";

export function ReviewForm() {
  const { id } = useParams<{ id: string }>();
  return (
    <form action={`/api/products/${id}/reviews`} method="post">
      <textarea name="text" />
      <button>Post review</button>
    </form>
  );
}

A client widget that needs the product id: const { id } = useParams<{ id: string }>() inside a review form nested deep in the product page.

useSelectedLayoutSegment

The active child segment one level below the layout that calls it.

  • Call it in a Client Component used by a layout; it returns the segment directly below that layout.
  • It returns null when the layout's own page is active.
  • Pass a parallel route key to read a slot's active segment.

Example: app/settings/tabs.tsx

"use client";
import Link from "next/link";
import { useSelectedLayoutSegment } from "next/navigation";

const tabs = [{ slug: "profile", label: "Profile" }, { slug: "billing", label: "Billing" }];

export function SettingsTabs() {
  const segment = useSelectedLayoutSegment(); // 'billing' on /settings/billing
  return (
    <nav role="tablist">
      {tabs.map(t => <Link key={t.slug} role="tab" aria-selected={segment === t.slug} href={`/settings/${t.slug}`}>{t.label}</Link>)}
    </nav>
  );
}

Tabs in a settings layout: useSelectedLayoutSegment() returns 'billing' on /settings/billing, which marks the Billing tab active.

useSelectedLayoutSegments

All active segments below the calling layout — how breadcrumbs are built.

  • It returns an array of every active segment below the calling layout.
  • Route groups such as (shop) appear in the result; filter them out for display.
  • Use it for breadcrumbs or nested navigation state.

Example: app/docs/breadcrumbs.tsx

"use client";
import Link from "next/link";
import { useSelectedLayoutSegments } from "next/navigation";

export function Breadcrumbs() {
  const segments = useSelectedLayoutSegments().filter(s => !s.startsWith("("));
  return (
    <nav aria-label="Breadcrumb">
      <Link href="/docs">Docs</Link>
      {segments.map((s, i) => <span key={s}> › <Link href={`/docs/${segments.slice(0, i + 1).join("/")}`}>{s}</Link></span>)}
    </nav>
  );
}

Breadcrumbs: On /docs/api/cookies a docs layout gets ['api', 'cookies'] and renders Docs › Api › Cookies.

useLinkStatus

Reports whether a parent <Link> navigation is pending, for inline loading indicators.

  • Import it from next/link; call it in a component rendered inside the <Link>.
  • pending is true only while that link's navigation is in progress.
  • Prefer loading.js and prefetching first; use this for subtle inline feedback.

Example: components/nav-item.tsx

"use client";
import Link, { useLinkStatus } from "next/link";

function Pending() {
  const { pending } = useLinkStatus();
  return <span aria-hidden className={pending ? "spinner" : "spinner hidden"} />;
}

export function NavItem({ href, label }: { href: string; label: string }) {
  return <Link href={href}>{label} <Pending /></Link>;
}

A spinner on the clicked link: function Hint() { const { pending } = useLinkStatus(); return pending ? <Spinner /> : null } placed inside each nav link.

useReportWebVitals

Receives Core Web Vitals measurements for forwarding to analytics.

  • It receives metrics such as LCP, INP and CLS, each with name, value and id.
  • Put it in a small Client Component rendered from the root layout.
  • Send values with navigator.sendBeacon so reports survive page unloads.

Example: app/web-vitals.tsx

"use client";
import { useReportWebVitals } from "next/web-vitals";

export function WebVitals() {
  useReportWebVitals(metric => {
    navigator.sendBeacon("/api/vitals", JSON.stringify({ name: metric.name, value: metric.value, page: location.pathname }));
  });
  return null;
}

Real-user performance monitoring: useReportWebVitals(m => navigator.sendBeacon('/api/vitals', JSON.stringify(m))).

useOffline

Returns true when the app has lost connectivity. Experimental: always false unless experimental.useOffline is on.

  • It is true after a network request fails or the browser fires offline.
  • It is always false on the server and before hydration.
  • The same flag retries blocked navigations, prefetches and Server Actions when the connection returns.

Example: next.config.ts

import type { NextConfig } from "next";
export default { experimental: { useOffline: true } } satisfies NextConfig;

An offline banner: const offline = useOffline(); return offline ? <div role="status">You are offline</div> : null.

next.config keys

images

Remote patterns, device sizes, formats and the loader. Remote hosts must be allowlisted.

  • remotePatterns lists allowed hosts with protocol, hostname and optional path.
  • formats: ['image/avif', 'image/webp'] turns on AVIF for smaller files.
  • unoptimized: true serves originals — needed for output: 'export' without a loader.

Allowing a CMS image host: images: { remotePatterns: [new URL('https://cdn.cms.com/**')] }.

redirects / rewrites

Declarative URL mapping evaluated before the filesystem routes.

  • Both are async functions returning arrays of { source, destination } with path patterns like /blog/:slug.
  • Redirects change the URL (307/308); rewrites keep it and serve other content.
  • Rewrites can proxy to an external URL — a common way to put an API under your domain.

Moving and proxying: { source: '/old-blog/:slug', destination: '/blog/:slug', permanent: true } and a rewrite of /api/:path* to the backend.

headers (config)

Static response headers per path pattern — the place for security headers.

  • Return { source, headers: [{ key, value }] } entries; they apply to matching paths.
  • Use it for Content-Security-Policy, Strict-Transport-Security and X-Content-Type-Options.
  • Headers needing a per-request value (a CSP nonce) belong in proxy.js instead.

Site-wide security headers: { source: '/(.*)', headers: [{ key: 'X-Content-Type-Options', value: 'nosniff' }] }.

env

Build-time inlined variables. Anything prefixed NEXT_PUBLIC_ ships to the browser.

  • Values are inlined at build time, so changing them needs a rebuild.
  • Prefer .env files; NEXT_PUBLIC_ variables are also inlined into browser bundles.
  • Never put secrets in a NEXT_PUBLIC_ variable — anyone can read them in the JavaScript.

A public analytics id: NEXT_PUBLIC_ANALYTICS_ID=abc is safe in the browser; DATABASE_URL stays server-only.

basePath / assetPrefix

Serve the app under a sub-path, or its assets from a CDN.

  • basePath: '/docs' prefixes every route and <Link>; it is fixed at build time.
  • assetPrefix only moves /_next/static files to a CDN domain.
  • Files in public/ are not affected by assetPrefix.

Hosting docs under a sub-path: basePath: '/docs' serves the app at example.com/docs, and <Link href="/intro"> goes to /docs/intro.

output

standalone for a minimal deployable server, export for a fully static site.

  • 'standalone' copies only needed files into .next/standalone — ideal for small Docker images.
  • 'export' produces static HTML in out/; server features like Server Actions and cookies are unavailable.
  • With standalone, copy public and .next/static yourself — they are not included.

A small Docker image: output: 'standalone' then COPY .next/standalone ./ and run node server.js — no full node_modules needed.

serverExternalPackages

Packages to leave unbundled on the server, usually because they load native binaries.

  • Listed packages are loaded with Node's require instead of being bundled.
  • Many common native packages (sharp, Prisma engines…) are already on the built-in list.
  • Add a package here when bundling breaks it — native binaries, dynamic requires.

Using a native PDF library: serverExternalPackages: ['pdf-native-lib'] fixes "Module not found" errors from a package that loads .node files.

transpilePackages

Local or workspace packages shipped as untranspiled source.

  • Needed for monorepo packages that ship TypeScript or JSX source.
  • It replaces the old next-transpile-modules plugin.
  • List packages by name, e.g. transpilePackages: ['@acme/ui'].

A shared UI package in a monorepo: The apps/web Next.js app imports @acme/ui TypeScript components directly from packages/ui/src.

optimizePackageImports

Rewrites barrel imports so only the used modules are bundled.

  • Libraries such as lucide-react and date-fns are optimized by default.
  • Add your own barrel-file packages to cut dev compile time and bundle size.
  • It only helps packages that re-export many modules from one entry.

An internal icon package: optimizePackageImports: ['@acme/icons'] makes import { Bell } from '@acme/icons' load only the Bell module.

reactCompiler

Enables the React Compiler, which memoises automatically and retires most useMemo.

  • reactCompiler: true in next.config, with babel-plugin-react-compiler installed.
  • It skips components that break the Rules of React rather than miscompiling them.
  • It increases build time, so measure on large projects.

Removing manual memoization: With the compiler on, useMemo and useCallback around list filters can be removed — the compiler memoizes them.

reactStrictMode

Double-invokes render in development to surface impure components.

  • It is on by default in the App Router.
  • Effects run, clean up and run again in development to catch missing cleanup.
  • It has no effect in production builds.

Finding a subscription leak: A chat component's double connection in dev reveals a missing return () => socket.close() in its Effect.

typedRoutes

Generates route types so <Link href> is checked against routes that exist.

  • Set typedRoutes: true; types are generated during next dev, next build or next typegen.
  • A typo in <Link href="/dashbord"> becomes a TypeScript error.
  • Dynamic hrefs need as Route or template literals that match a real route.

Catching broken links in CI: next typegen && tsc --noEmit fails the pipeline when a link points to a route that was deleted.

typescript / eslint

typescript.ignoreBuildErrors decides whether type errors fail the build. The eslint key was removed in 16.

  • typescript.ignoreBuildErrors: true lets a build pass with type errors — a risky shortcut.
  • typescript.tsconfigPath points the build at a different tsconfig.
  • Linting is no longer part of next build; run ESLint or Biome as its own CI step.

Splitting checks in CI: The pipeline runs eslint ., then tsc --noEmit, then next build — each failure is reported separately.

logging

Controls fetch and cache logging in development, including full URLs.

  • logging.fetches.fullUrl: true prints full URLs for fetches in the dev terminal.
  • logging.incomingRequests can log every request in development.
  • logging: false turns dev logging off.

Debugging cache misses: logging: { fetches: { fullUrl: true } } shows which fetches hit the cache and which went to the network.

devIndicators

The on-screen build and render indicators shown during development.

  • The indicator shows whether a route is static or dynamic while you develop.
  • devIndicators: false hides it; position moves it to another corner.
  • It appears only in next dev, never in production.

Checking that a page stays static: The badge switching to dynamic after adding cookies() warns that the page lost its static shell.

allowedDevOrigins

Origins permitted to reach the dev server, which is otherwise same-origin only.

  • The dev server blocks cross-origin requests to its internal assets by default.
  • Add hostnames such as 'local-origin.dev' or '*.local-origin.dev'.
  • Needed when testing from a phone on the LAN or through a tunnel.

Testing on a phone: allowedDevOrigins: ['192.168.1.20'] lets the phone load HMR and dev assets from your laptop.

compress

gzip for responses from the Next server. Turn it off when a proxy already compresses.

  • It is true by default for the Next.js server.
  • Turn it off when Nginx or a CDN already compresses, to avoid doing the work twice.
  • It only affects next start, not static exports.

Behind Nginx: compress: false because Nginx already applies gzip and Brotli.

poweredByHeader

Removes X-Powered-By, which otherwise advertises the framework.

  • Set poweredByHeader: false to drop X-Powered-By: Next.js.
  • Hiding the framework is a small hardening step, not real security.
  • It is commonly part of a security checklist.

Passing a security scan: A scanner flags X-Powered-By; one config line removes it.

trailingSlash

Whether URLs end with a slash, and the redirect that enforces the choice.

  • trailingSlash: true redirects /about to /about/.
  • With output: 'export' it also changes the files: /about/index.html instead of /about.html.
  • Pick one form and keep it, so search engines do not see duplicate URLs.

Matching a legacy site's URLs: A migrated site uses trailingSlash: true so existing /products/ links keep working without extra redirects.

pageExtensions

Which extensions count as routable pages — how .mdx routes are enabled.

  • The default is ['tsx', 'ts', 'jsx', 'js'].
  • Add 'md' and 'mdx' to make Markdown files routable with @next/mdx.
  • Every file convention — layout, page, route — uses the same extension list.

MDX pages: pageExtensions: ['ts', 'tsx', 'mdx'] turns app/blog/hello/page.mdx into a page.

serverActions

Body size limit and the allowed origins for Server Function invocations.

  • bodySizeLimit defaults to 1MB — raise it for file uploads.
  • allowedOrigins lists extra origins allowed to call actions, such as a reverse proxy domain.
  • Origin checks protect Server Actions against CSRF by default.

Uploading images through a form: serverActions: { bodySizeLimit: '5mb' } lets an avatar upload action accept larger files.

staticGeneration

Concurrency, retries and timeouts for the static generation pass.

  • staticGenerationRetryCount retries failed page generation before failing the build.
  • staticGenerationMaxConcurrency limits pages per worker.
  • These options are experimental and for large or flaky builds.

A build that hits a slow CMS: staticGenerationRetryCount: 3 stops one timeout from failing a 10,000-page build.

partialPrefetching

Prefetches the static shell of a dynamic route ahead of navigation.

  • It requires cacheComponents; otherwise the config fails validation.
  • Instead of one prefetch per link, one reusable App Shell is prefetched per route.
  • <Link prefetch={true}> still fetches URL-specific data too.

A product grid with 100 links: With partialPrefetching: true, 100 links to /p/[id] cost one shared shell prefetch, not 100 separate ones.

prefetchInlining

Inlines small prefetch payloads into the HTML rather than fetching them.

  • Inlining is on by default; this experimental option only tunes or disables it.
  • maxSize and maxBundleSize are measured in gzip-compressed bytes.
  • Turn it off when debugging navigation or measuring request counts.

Investigating prefetch traffic: experimental: { prefetchInlining: false } makes each segment its own request so it can be seen in DevTools.

cssChunking

How CSS is split across chunks, trading request count against duplication.

  • true (default) splits and merges CSS files only as needed.
  • 'strict' loads CSS in import order — use it when styles depend on order.
  • false turns chunking off.

Fixing style-order bugs: experimental.cssChunking: 'strict' keeps a reset stylesheet before component styles.

inlineCss

Inlines critical CSS into the document to cut a render-blocking request.

  • It replaces <link> stylesheet tags with <style> tags in the HTML.
  • Good for small CSS like Tailwind output and first visits; bad for large CSS that should be cached.
  • It is an experimental, production-only option.

A landing page with small CSS: experimental.inlineCss: true removes one render-blocking request for first-time visitors.

taint

Enables React's taint APIs, so a flagged object or value cannot cross to the client.

  • experimental.taint: true enables React's taintObjectReference and taintUniqueValue.
  • A tainted value throws if it is passed to a Client Component.
  • It is a safety net, not a replacement for returning only the fields the UI needs.

Keeping tokens off the client: taintUniqueValue('Do not pass tokens', user, user.apiToken) makes an accidental <Client token={user.apiToken} /> throw.

proxyClientMaxBodySize

How much of a request body proxy.js buffers (default 10MB). Beyond it the body is truncated with a warning, not rejected.

  • The body is buffered so both proxy.js and the route can read it.
  • Set it with units: experimental: { proxyClientMaxBodySize: '50mb' }.
  • Truncated requests keep going, so endpoints that need the full body must handle uploads differently.

Large uploads through proxy: Raising the limit to '50mb' lets a file-upload route behind proxy.ts receive the whole file.

htmlLimitedBots

User agents served blocking metadata instead of streamed HTML.

  • The default list already covers Googlebot variants, Bingbot, Twitterbot and Slackbot.
  • Setting it replaces the default list, so include the defaults you still need.
  • A regex matching all user agents turns off streaming metadata entirely.

A crawler that misses titles: Adding a niche SEO crawler's user agent makes it receive <title> in the initial HTML.

webpack

The escape hatch for custom bundling. Turbopack is the default and needs it far less often.

  • The function receives the config plus { isServer, dev, buildId } and must return the config.
  • It is ignored when Turbopack runs, which is the default in 16.
  • Run next build --webpack to keep using it during migration.

A custom loader during migration: A project that still needs a webpack-only plugin builds with --webpack while equivalent turbopack.rules are written.

Turbopack config

turbopack

Resolve aliases, extensions and loader rules for the default bundler.

  • turbopack.rules maps file globs to webpack-compatible loaders, such as SVGR for *.svg.
  • turbopack.resolveAlias replaces a module, for example aliasing fs to an empty module in the browser.
  • It is a top-level key in Next.js 16, no longer under experimental.

Importing SVGs as components: turbopack: { rules: { '*.svg': { loaders: ['@svgr/webpack'], as: '*.js' } } }.

turbopackFileSystemCache

Persists the build cache to disk so restarts and CI runs reuse work.

  • Both dev and build caches are on by default, under .next/dev/cache and .next/cache.
  • CI only benefits if .next/cache is restored before each build.
  • Set turbopackFileSystemCacheForBuild: false to opt out.

Faster CI builds: Caching .next/cache between GitHub Actions runs keyed on the lockfile makes later builds start warm.

turbopackChunking

Tunes how modules are grouped into chunks.

  • It tunes the production JavaScript chunker's size thresholds.
  • It is experimental and most apps should keep the defaults.
  • Measure bundle output before and after changing it.

A page with too many small chunks: Raising the minimum chunk size merges tiny chunks and reduces the number of requests.

turbopackMemoryEviction

Bounds compiler memory on large projects by evicting cached work.

  • 'auto' (default) evicts after disk snapshots when enough memory has built up.
  • 'full' evicts everything after each snapshot; false keeps all data in memory.
  • It only matters in next dev with the filesystem cache enabled.

A dev server running out of memory: On a large monorepo, 'full' keeps the dev process's memory flat at the cost of some reload speed.

turbopackIgnoreIssue

turbopack.ignoreIssue hides a specific known Turbopack warning by file path, instead of disabling a whole check.

  • Each rule matches a file path (glob or regex) and optionally title or description.
  • Matching issues disappear from the CLI and the error overlay.
  • Prefer path patterns — issue titles can change between versions.

Silencing a known warning from a dependency: ignoreIssue: [{ path: '**/node_modules/legacy-lib/**' }] hides a harmless warning without hiding your own.

CLI & adapters

next dev

The development server, Turbopack by default, with HMR and error overlays.

  • Turbopack is the default bundler; pass --webpack to use webpack.
  • --port and --hostname change where it listens; --experimental-https serves locally over HTTPS.
  • Prefetching is off in dev, so navigation can feel slower than in production.

Testing a secure-context API: next dev --experimental-https creates a local certificate so clipboard and camera APIs work.

next build

The production build, including static generation and the route size report.

  • It no longer runs the linter — lint separately.
  • The output marks each route as static or dynamic.
  • --debug-prerender helps find what made a route dynamic.

Checking a page stayed static: After a change, the build output shows /pricing is still static — no request-time API slipped in.

next start

Serves a production build. Not used when deploying output: 'standalone' directly.

  • It needs a completed next build in the same folder.
  • -p sets the port; PORT works too.
  • Use it for self-hosting on a Node server or container.

Running in production on a VM: next build && next start -p 3000 behind Nginx, managed by systemd or PM2.

next lint

Removed in Next.js 16. Run the ESLint CLI with eslint-config-next directly; next build no longer lints.

  • Install eslint and eslint-config-next, then extend next/core-web-vitals in eslint.config.mjs.
  • Add "lint": "eslint ." to package.json scripts.
  • A codemod migrates projects: npx @next/codemod@canary next-lint-to-eslint-cli ..

Upgrading lint after Next.js 16: The CI step changes from next lint to npm run lint, which runs ESLint with the same Next.js rules.

create-next-app

Scaffolds a new project with TypeScript, Tailwind, ESLint and App Router choices.

  • npx create-next-app@latest asks about TypeScript, Tailwind, ESLint or Biome, src/ and the import alias.
  • --yes accepts the recommended defaults.
  • --example starts from any official example.

Starting a project in one command: npx create-next-app@latest shop --yes creates a TypeScript, Tailwind, App Router project ready for npm run dev.

Adapters

The deployment adapter API — build output, routing information and runtime integration for a host.

  • An adapter implements modifyConfig and onBuildComplete to turn build output into a host's format.
  • It is set with adapterPath in next.config or the NEXT_ADAPTER_PATH variable.
  • It receives routing information and output types so the host can match Next.js behaviour.

Deploying to a custom platform: A company's adapter reads onBuildComplete output and maps each route to its own function and CDN rules.

Ask follow-up questions on any topic in your dashboard.

Start learning