Next.js
The React framework for fast, SEO-friendly sites: App Router, server rendering and caching.
In 30 seconds
Next.js is a framework built on React that handles routing, server rendering, data fetching, caching, images and fonts for you. Pages can be generated ahead of time, rendered per request, or a mix, so users get fast HTML and search engines see real content. You write React components; Next.js decides how to build, serve and cache them.
Why I use it
- I build storefronts, marketing sites and web apps with Next.js because it gives me server-rendered, SEO-friendly pages without giving up React's component model.
- I rewrote a large agency site from the old Pages Router to the App Router, so I know both models and the migration path between them.
- I value that performance features (image optimisation, self-hosted fonts, code splitting, streaming) are on by default instead of hand-built per project.
- I run Next.js in production on my own Linux servers behind nginx, so I understand what the framework needs outside a managed platform.
How it works
01The App Router: files become routes
In the App Router, folders inside app/ define URL segments, and special files define what each segment shows. page.tsx makes a route publicly reachable. layout.tsx wraps a segment and all its children, and it stays mounted while users navigate between child pages, so headers, sidebars and state persist. loading.tsx is shown instantly while the page streams in (it is a Suspense boundary), error.tsx catches errors in that segment, and not-found.tsx renders 404 content. Dynamic segments use square brackets, such as [slug]; route groups in parentheses, such as (shop), organise files without changing the URL. route.ts creates an API endpoint (a route handler) with GET or POST functions. Navigation uses the <Link> component, which prefetches routes in the background so clicks feel instant. Since Next.js 15, params and searchParams are promises that you await.
app/
layout.tsx // root layout: <html>, <body>, fonts
page.tsx // "/"
(shop)/
layout.tsx // shared shop layout, URL unchanged
products/
page.tsx // "/products"
loading.tsx // streamed skeleton
[slug]/
page.tsx // "/products/blue-shoe"
not-found.tsx
api/
health/route.ts // GET /api/health
sitemap.ts // /sitemap.xml
robots.ts // /robots.txt
02Server and Client Components in Next.js
Every component in the app directory is a Server Component by default. It runs on the server, can be async, can query a database or call an internal API with secrets, and sends no JavaScript of its own to the browser. When you need interactivity (state, effects, event handlers, browser APIs), create a Client Component by putting "use client" at the top of the file. That directive marks a boundary: the file and everything it imports become part of the client bundle. So push "use client" as far down the tree as possible, for example making just the 'Add to cart' button a client component rather than the whole product page. Server Components can pass serialisable props (strings, numbers, plain objects, promises) to Client Components and can pass server-rendered content into them as children. For mutations, Server Actions (functions marked "use server") handle form submissions without a hand-written API route.
// app/products/[slug]/page.tsx (Server Component)
import { notFound } from "next/navigation";
import { AddToCart } from "./add-to-cart"; // has "use client" at the top
export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const res = await fetch(`https://api.example.com/products/${slug}`);
if (!res.ok) notFound();
const product: { id: string; name: string; description: string } = await res.json();
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCart productId={product.id} />
</article>
);
}
03Rendering strategies: SSG, SSR, ISR and streaming
Next.js chooses how to render each route. Static generation (SSG) builds HTML at build time; it is the fastest option and is served from a CDN, ideal for marketing pages and blog posts. generateStaticParams tells Next.js which dynamic pages, such as product slugs, to pre-build. Server-side rendering (SSR) renders on every request, needed when a page depends on cookies, headers or per-user data. Incremental Static Regeneration (ISR) is the middle ground: serve a static page, and regenerate it in the background after a time (export const revalidate = 3600) or when you call revalidatePath or revalidateTag after content changes. Streaming lets the server send the page shell immediately and fill slower parts later, through Suspense boundaries and loading.tsx. Partial Prerendering combines both: a static shell plus dynamic holes, enabled in Next.js 16 through the Cache Components model.
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // ISR: regenerate at most once an hour
export async function generateStaticParams() {
const posts: { slug: string }[] = await fetch("https://cms.example.com/posts")
.then((r) => r.json());
return posts.map((p) => ({ slug: p.slug })); // pre-built at build time
}
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function publishPost(slug: string) {
// ...save to the CMS or database...
revalidatePath(`/blog/${slug}`); // refresh that page on the next request
}
04Caching and revalidation, and what changed in Next.js 16
Caching is where Next.js versions differ most. Next.js 14 cached fetch requests by default, which surprised many teams. Next.js 15 reversed this: fetch, GET route handlers and client-side page navigations are no longer cached by default, and you opt in with fetch(url, { next: { revalidate: 60, tags: ['products'] } }), unstable_cache or route segment options. Next.js 16 introduced Cache Components, enabled with cacheComponents: true. Instead of guessing what is static, you mark cached work explicitly with the "use cache" directive on a function, component or page, set its lifetime with cacheLife and label it with cacheTag; everything uncached must sit inside a Suspense boundary and is rendered at request time. Invalidation also changed: revalidateTag now takes a cacheLife profile as a second argument (stale-while-revalidate), the new updateTag expires and refreshes immediately inside a Server Action (read-your-writes), and refresh() refreshes the client router.
// next.config.ts (Next.js 16)
// const nextConfig = { cacheComponents: true };
// lib/products.ts
import { cacheLife, cacheTag } from "next/cache";
export async function getProducts() {
"use cache";
cacheLife("hours"); // built-in profile
cacheTag("products");
const res = await fetch("https://api.example.com/products");
return res.json();
}
// app/admin/actions.ts
"use server";
import { revalidateTag, updateTag } from "next/cache";
export async function onCatalogImport() { revalidateTag("products", "max"); } // background refresh
export async function onPriceEdit() { updateTag("products"); } // admin sees change now
05Metadata and SEO
Search engines and social networks read the HTML <head>, and the App Router generates it from your code. Export a static metadata object, or an async generateMetadata function for dynamic pages, from any layout or page. Set a title template in the root layout ("%s | Example Shop") so every page gets a consistent suffix, and set metadataBase once so relative Open Graph image URLs become absolute. For each page, provide a unique title and description, a canonical URL to avoid duplicate content, and alternates.languages (hreflang) for translated versions. File conventions cover the rest: sitemap.ts, robots.ts and opengraph-image.tsx generate those files at build or request time. Add structured data (JSON-LD, such as Product or Article) as a script tag in the page so search engines can show rich results. Because the HTML is server-rendered, crawlers see full content without running JavaScript.
// app/products/[slug]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const product = await fetch(`https://api.example.com/products/${slug}`).then((r) => r.json());
return {
title: product.name, // root layout template adds " | Example Shop"
description: product.summary,
alternates: {
canonical: `/products/${slug}`,
languages: { en: `/en/products/${slug}`, ru: `/ru/products/${slug}` },
},
openGraph: { images: [product.imageUrl] },
};
}
06Images, fonts and why Next.js sites are fast
Large images and web fonts are the usual causes of slow pages and layout shift. next/image resizes and converts images to modern formats such as WebP or AVIF on demand, generates srcset so phones download small files, lazy-loads below-the-fold images, and reserves space using width and height (or fill with a sized parent) so the layout does not jump. For the hero image, which is usually the Largest Contentful Paint, mark it to load eagerly: priority in Next.js 15, replaced by preload in Next.js 16. next/font downloads Google or local fonts at build time and serves them from your own domain, with no request to Google at runtime, and generates a size-adjusted fallback so text does not reflow when the font arrives. Combined with server rendering, automatic code splitting per route, link prefetching and streaming, this is why Next.js sites score well on Core Web Vitals out of the box.
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin", "cyrillic"], display: "swap", variable: "--font-main" });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.variable}>
<body>{children}</body>
</html>
);
}
// app/page.tsx
import Image from "next/image";
export default function Home() {
return (
<Image src="/hero.jpg" alt="Fresh coffee beans" width={1600} height={900}
sizes="100vw" preload /> // Next.js 15: use priority instead of preload
);
}
07Deployment options and Next.js 15 vs 16
Vercel, the company behind Next.js, offers zero-configuration hosting with a CDN, serverless functions and image optimisation. Other platforms (Netlify, Cloudflare via OpenNext, AWS Amplify) support it through adapters; Next.js 16.2 made the Build Adapters API stable for this. You can also self-host: next build followed by next start runs a Node.js server, usually managed by pm2 or a container and placed behind nginx for TLS and compression. Setting output: "standalone" produces a minimal folder that is ideal for Docker images. When self-hosting with several instances, configure a shared cache handler so ISR data stays consistent. Key Next.js 16 changes compared with 15: Turbopack is the default bundler for dev and build, middleware.ts is renamed proxy.ts (Node.js runtime), synchronous params and cookies() access is removed, Node.js 20.9+ is required, next lint is gone, and next/image defaults are stricter (quality 75 only and a longer minimum cache time).
// next.config.ts - self-hosted build for Docker
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
images: {
remotePatterns: [{ protocol: "https", hostname: "cdn.example.com" }],
},
};
export default nextConfig;
// Build and run:
// npm ci && npm run build
// node .next/standalone/server.js (copy public/ and .next/static/ alongside)
// Behind nginx: proxy_pass http://127.0.0.1:3000;
Use it when
- Public sites where SEO and first-load speed matter: e-commerce storefronts, marketing sites, blogs, documentation.
- Products that mix static content with personalised or dynamic parts, such as a shop with a cart and account area.
- Teams already using React that want routing, rendering and performance conventions instead of assembling them by hand.
- Full-stack features with a light backend: forms, webhooks and small APIs through Server Actions and route handlers.
Skip it when
- Internal dashboards behind a login with no SEO needs, where a plain React + Vite single-page app is simpler to build and host.
- Heavy backend domains (complex business rules, queues, many integrations); keep a dedicated API (Laravel, NestJS) and use Next.js as the frontend.
- Teams that cannot run Node.js anywhere and only need static pages; a static site generator like Astro may be lighter.
Alternatives
- Remix / React Router 7
React framework centred on web standards, loaders and actions; strong for data-heavy apps.
- Astro
Content-first sites that ship zero JavaScript by default, with islands of React, Vue or Svelte.
- Nuxt or SvelteKit
Equivalent full-stack frameworks for Vue and Svelte.
- React + Vite (SPA)
Client-only React; simplest for authenticated apps that do not need SEO.
Best practices
- Keep components on the server by default and add "use client" only to the interactive leaves.
- Be explicit about caching: decide per data source how fresh it must be and how it is invalidated (tags, paths).
- Generate metadata, canonical URLs, hreflang, sitemap and robots for every public page.
- Use next/image with correct sizes and next/font for every image and font; preload only the hero image.
- Wrap slow data in Suspense boundaries so the shell streams immediately.
- Validate input in Server Actions and route handlers exactly as you would in any API; they are public endpoints.
- Pin the major version and follow the official upgrade guide and codemods when moving between 15 and 16.
Common mistakes
- Putting "use client" at the top of a page or layout, which pulls the whole tree into the browser bundle.
- Assuming data is cached (or not cached) without checking the rules for your Next.js version.
- Reading cookies() or headers() in a root layout and accidentally making every page dynamic.
- Using plain <img> tags without dimensions, causing layout shift and oversized downloads.
- Treating Server Actions as private functions and skipping authentication and validation.
Deep dive
08The RSC payload, the client router and its cache
A Next.js page response contains two things: HTML for the first paint, and the RSC payload, a compact serialised description of the Server Component tree with placeholders where Client Components go, plus references to their JavaScript files. On the first load the browser shows the HTML, then React uses the payload to hydrate the Client Components. On later navigations with <Link>, Next.js does not fetch HTML at all; it requests only the RSC payload for the new route segments and merges it into the current tree, so shared layouts keep their state and are not re-rendered. Prefetched and visited segments are kept in an in-memory client router cache. Since Next.js 15, page segments are not reused from that cache by default (layouts and loading states still are), which avoids stale pages; experimental.staleTimes tunes this. After a mutation, call revalidatePath or updateTag on the server, or router.refresh() on the client, so the client cache does not show stale data. Understanding this split explains why a full page reload sometimes 'fixes' a bug: it discards the client cache.
"use client";
import { useRouter } from "next/navigation";
import { useTransition } from "react";
export function MarkShippedButton({ orderId }: { orderId: string }) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
async function markShipped() {
await fetch(`/api/orders/${orderId}/ship`, { method: "POST" });
startTransition(() => router.refresh()); // refetch RSC payload, keep client state
}
return <button onClick={markShipped} disabled={isPending}>Mark shipped</button>;
}
// next.config.ts (optional): experimental: { staleTimes: { dynamic: 30, static: 180 } }
09Server Actions and route handlers as a security boundary
A Server Action looks like a function call, but it compiles to a public POST endpoint that anyone can call with any arguments. Next.js adds some protection: actions get unguessable ids, unused actions are removed from the build, closed-over variables are encrypted before being sent to the client, and the Origin header is compared with the host to block cross-site request forgery. If you deploy behind a proxy or CDN with a different host, list it in experimental.serverActions.allowedOrigins rather than disabling the check. Request bodies are limited to 1 MB by default (serverActions.bodySizeLimit). None of this replaces your own checks: in every action, authenticate the user, authorise access to the specific record, and validate arguments with a schema, exactly as in a REST controller. Keep data-access code in a separate module marked with import 'server-only', so it can never be bundled into client code by accident. When running several instances, set the same NEXT_SERVER_ACTIONS_ENCRYPTION_KEY on all of them so encrypted values survive between servers.
"use server";
import "server-only";
import { z } from "zod";
import { updateTag } from "next/cache";
import { getSession } from "@/lib/auth";
import { db } from "@/lib/db";
const Input = z.object({ productId: z.string().uuid(), price: z.number().int().positive() });
export async function updatePrice(raw: unknown) {
const session = await getSession();
if (!session?.user.isAdmin) throw new Error("Forbidden"); // authorise every call
const { productId, price } = Input.parse(raw); // never trust arguments
await db.product.update({ where: { id: productId }, data: { price } });
updateTag(`product-${productId}`);
}
10Proxy, runtimes and request-time work
In Next.js 16, middleware.ts is renamed proxy.ts and its exported function is called proxy. It runs before routing for every matched request, on the Node.js runtime, and is meant for lightweight decisions: redirects, rewrites, locale detection, setting headers, or a quick check that a session cookie exists. It should not be your only authorisation layer, because a misconfigured matcher or a bug can skip it; check permissions again close to the data. Keep it fast, since it adds latency to every request it matches, and use the matcher config to exclude static files and images. Beyond the proxy, several APIs make a route dynamic: cookies(), headers(), connection() and searchParams. Calling them in a root layout makes the entire site request-time rendered, so read them as deep in the tree as possible and wrap that part in Suspense. after() lets you schedule work such as logging or analytics to run after the response has been sent, so it does not slow the user down.
// proxy.ts (Next.js 16; was middleware.ts)
import { NextResponse, type NextRequest } from "next/server";
export function proxy(request: NextRequest) {
const hasSession = request.cookies.has("session");
if (!hasSession && request.nextUrl.pathname.startsWith("/account")) {
const url = new URL("/login", request.url);
url.searchParams.set("next", request.nextUrl.pathname);
return NextResponse.redirect(url);
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)"],
};
11Self-hosting at scale: shared caches, version skew and bundle size
One Next.js server on one machine is simple. Several instances behind a load balancer raise three problems. Cache consistency: by default each instance keeps its ISR and data cache on local disk and memory, so users may see different versions of a page depending on which server answers. Configure a custom cacheHandler backed by Redis or another shared store, and disable the in-memory cache with cacheMaxMemorySize: 0 if every read should go through it. Version skew: during a rolling deploy, a browser holding the old JavaScript may call a new server. Build once and ship the same artefact to every instance, set a deploymentId (or NEXT_DEPLOYMENT_ID), which Next.js adds to asset URLs and cache keys, and keep the previous build's static files available for a while, ideally on a CDN. Bundle size: in Next.js 16.1+, next experimental-analyze inspects Turbopack production bundles, so you can see which import pulled a large library into a client component. Cache /_next/static aggressively, since those files are content-hashed.
// next.config.ts - multi-instance self-hosting
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
deploymentId: process.env.GIT_SHA, // same value on every instance
cacheHandler: require.resolve("./cache-handler.mjs"), // shared store, e.g. Redis
cacheMaxMemorySize: 0, // no per-instance memory cache
};
export default nextConfig;
// nginx: content-hashed assets can be cached for a year
// location /_next/static/ { proxy_pass http://app; add_header Cache-Control "public, max-age=31536000, immutable"; }
// Inspect client bundles (Next.js 16.1+):
// npx next experimental-analyze
Real-world scenarios
Product prices update in the admin but not on the storefront
Problem
Editors change a price in the admin panel, but the product page keeps showing the old price for up to an hour. Sometimes a refresh shows the new price and the next refresh shows the old one.
How to solve it
Two problems combine here. The page is cached with a time-based revalidate and nothing invalidates it on change, so add a cacheTag per product and call revalidateTag (or updateTag inside a Server Action) when the admin saves; if the admin is a separate backend, expose a small authenticated route handler it can call as a webhook. The flip-flopping between old and new means several instances with local caches. Configure a shared cacheHandler in Redis so all instances read the same entries, and set the same deploymentId everywhere. Then verify by saving a price and reloading repeatedly through the load balancer.
The whole site became dynamic after adding a cart badge
Problem
A developer added a cart counter to the header, reading cookies() in the root layout. Build output now shows every route as dynamic, the time to first byte doubled, and CDN caching stopped working.
How to solve it
Reading cookies in the root layout opts every page into request-time rendering. Move the cookie read into a small CartBadge component and render it inside a Suspense boundary, with a neutral fallback such as an empty badge. With Cache Components enabled, the static shell, including the header, is then prerendered and the badge streams in per request (Partial Prerendering). Without Cache Components, an alternative is a client component that fetches the count from a route handler after load. Check the build output again: product and content pages should be static or ISR, and only the badge, cart and account routes dynamic.
A self-hosted deploy breaks navigation for active users
Problem
After each release, some users who had the site open see errors when clicking links or submitting forms, until they reload. Logs show 404s for JavaScript chunks and failed Server Action calls.
How to solve it
This is version skew: old pages request chunks or action ids that the new build no longer has. Build once in CI and deploy the same artefact everywhere. Set deploymentId from the git commit so Next.js can detect the mismatch and reload the page when needed. Keep previous builds' /_next/static files available for a few days, for example by uploading them to a CDN or object storage with assetPrefix, so old pages can still load their chunks. Set a fixed NEXT_SERVER_ACTIONS_ENCRYPTION_KEY across releases and instances. Prefer additive changes to Server Action signatures, and monitor 404s on /_next/static after each release.
Cheat sheet
- Start a new app
npx create-next-app@latest my-app --ts --app- Upgrade with official codemods
npx @next/codemod@canary upgrade latest- See which routes are static or dynamic
npm run build // read the route table in the output- Inspect client bundles (16.1+)
npx next experimental-analyze- Opt a component into request time explicitly
import { connection } from "next/server"; await connection();- Run work after the response is sent
import { after } from "next/server"; after(() => logView(id));- Keep a module out of client bundles
import "server-only";- Refresh server data from the client
router.refresh() // from next/navigation- Allow Server Actions behind a proxy domain
experimental: { serverActions: { allowedOrigins: ["shop.example.com"] } }- Consistent builds across instances
deploymentId: process.env.GIT_SHA
In my real projects
- I was the main contributor to a full rewrite of an agency website from the Pages Router to the App Router on Next.js 15, built in parallel for about a year and a half and then swapped in.
- The pet-supplies storefront runs on Next.js 16 with next-intl for three languages, per-page SEO and a sitemap, backed by a Laravel API.
- A coffee chain's careers website runs on Next.js 16 with a custom server for security and cache headers, plus generated sitemap, robots and Open Graph images.
- On my own servers, Next.js apps run under pm2 behind nginx, alongside the Laravel APIs that feed them.
Questions & answers
Q1What is the difference between SSR, SSG and ISR?
They differ in when HTML is generated. SSG renders pages at build time, so every visitor gets the same pre-built file from a CDN, which is the fastest option and suits content that rarely changes. SSR renders on each request, which is needed for personalised pages or data that must be fresh every time, at the cost of server work per visit. ISR combines them: pages are static but regenerated in the background after a time interval or on demand when content changes, through revalidatePath or revalidateTag. In the App Router you rarely pick a mode by name; it follows from the data you use, your caching settings and dynamic functions like cookies().
Q2How do Server Components and Client Components work together in Next.js?
Everything in the app directory is a Server Component unless marked otherwise. Server Components render on the server, can fetch data directly and keep secrets and heavy libraries out of the browser. A file starting with "use client" becomes a client boundary: it and its imports are bundled for the browser, hydrated and can use state, effects and events. Server Components can render Client Components and pass them serialisable props or server-rendered children, but Client Components cannot import Server Components directly. The goal is to keep the interactive parts small, so pages ship less JavaScript while still being fully interactive where needed.
Q3Why are Next.js sites good for SEO?
Mainly because crawlers receive complete HTML. With static or server rendering, titles, text, links and structured data are in the initial response, so search engines do not have to execute JavaScript to see the content. On top of that, Next.js gives built-in tools for the rest of technical SEO: the Metadata API for titles, descriptions, canonical URLs, hreflang and Open Graph tags; file conventions for sitemap.xml and robots.txt; generated social images; and strong Core Web Vitals through image optimisation, font handling, code splitting and streaming. Good SEO still needs good content and structure, but the framework removes most technical obstacles.
Q4What changed in caching between Next.js 14, 15 and 16?
Next.js 14 cached fetch results and route handlers by default, which made data unexpectedly stale. Next.js 15 flipped those defaults: fetch, GET route handlers and client navigations to pages are uncached unless you opt in with revalidate, tags, unstable_cache or segment config. Next.js 16 adds Cache Components behind cacheComponents: true, where caching is explicit: you mark functions or components with "use cache", set lifetimes with cacheLife and labels with cacheTag, and uncached data must be inside Suspense. revalidateTag now requires a cacheLife profile, updateTag gives read-your-writes refreshes in Server Actions, and Partial Prerendering is enabled through this model.
Q5Should we upgrade from Next.js 15 to 16?
Usually yes, but as a planned task. Next.js 16 brings Turbopack as the default bundler with much faster builds, the stable React Compiler option, a reworked router with leaner prefetching, React 19.2 features and the Cache Components model. The breaking changes are manageable: Node.js 20.9 or newer, async-only params, cookies() and headers(), middleware renamed to proxy, next lint removed, stricter next/image defaults and a new revalidateTag signature. The official codemod handles much of this. I upgrade on a branch, run the codemods, check custom webpack config and image usage, then test critical flows. Version 15 still receives backported fixes, so there is no emergency.
Q6How would you deploy a Next.js app outside Vercel?
Build with output: "standalone", which produces a minimal Node.js server with only the dependencies it needs, then run it in a Docker container or under a process manager like pm2. Put nginx in front for TLS, compression, and caching of the immutable /_next/static files, with a CDN such as Cloudflare ahead of that if needed. Set environment variables at runtime, keep secrets out of NEXT_PUBLIC_ variables, and add a health-check route. Image optimisation works on the server, but a CDN helps under load. If you run several instances, configure a shared cache handler, for example in Redis, so ISR and cached data stay consistent across them.
Senior-level questions
S1How would you design caching for a large e-commerce site built on Next.js?
I decide freshness per data source, not per page. Category and product content can be cached for hours and invalidated by tags when the catalogue changes, through a webhook from the backend. Prices and stock change more often and matter more, so they get short lifetimes or are fetched at request time in a small Suspense-wrapped component, keeping the rest of the page static. Cart, account and checkout are always dynamic and never cached on shared layers. With Cache Components this maps directly onto "use cache" with cacheLife and cacheTag, plus Partial Prerendering for the shell. On self-hosted infrastructure, a shared cache handler keeps instances consistent, and a CDN caches static assets and public pages. The trade-off is complexity: every cached item needs a clear owner and invalidation path, or stale prices become a support problem.
S2When would you not use Server Actions, and use route handlers or a separate API instead?
Server Actions are excellent for mutations initiated by your own Next.js UI, especially forms, because they integrate with transitions, progressive enhancement and cache invalidation. They are a poor fit when other clients need the same operation: a mobile app, a partner integration or a webhook needs a stable, documented HTTP contract, which route handlers or a dedicated backend provide. Action ids are build-specific, so they are not a public API. They also run serially from one client by default, so they are not ideal for parallel data fetching; reading data belongs in Server Components. When a business domain is large, with queues, scheduled jobs and many integrations, I keep that logic in a separate API, Laravel or NestJS, and let Server Actions be thin wrappers that call it.
S3Vercel or self-hosting: how do you advise a client?
It is a trade-off between operational effort and cost and control. Vercel gives the full feature set with no configuration: global CDN, image optimisation, preview deployments, skew protection and scaling. It is ideal for small teams and traffic patterns with peaks, but costs grow with bandwidth, function time and image transformations, and data residency or existing infrastructure can rule it out. Self-hosting on a VPS or containers with nginx and a CDN is cheaper at steady traffic and keeps everything in one place, but the team owns the parts Vercel automates: TLS, zero-downtime deploys, shared caches across instances, version skew, log collection and monitoring. Other platforms using adapters sit in between. For a client with an existing server team and predictable traffic I lean to self-hosting; for a small marketing team, the managed platform usually wins.
Glossary
- RSC payload
- The serialised Server Component tree that the client router uses to update the page without fetching HTML.
- Client router cache
- In-browser memory of prefetched and visited route segments, used to make navigation instant.
- Dynamic API
- A function such as cookies(), headers() or connection() that forces request-time rendering where it is used.
- Partial Prerendering
- Serving a prerendered static shell immediately and streaming dynamic parts into Suspense holes per request.
- Cache tag
- A label attached to cached data so all entries with that label can be invalidated together.
- Proxy (proxy.ts)
- Next.js 16's name for middleware: code that runs before routing to redirect, rewrite or set headers.
- Server Action
- A server function callable from the client, compiled to a POST endpoint; must validate and authorise itself.
- Version skew
- A browser running an old build's code talks to servers running a new build, causing missing chunks or actions.
- Standalone output
- A build mode that copies only the files and dependencies needed to run the server, ideal for Docker.
- Cache handler
- A pluggable module that stores Next.js cache entries, for example in Redis, shared by all instances.
Keep learning
Have a product in mind?Let's build it.
Tell me about your idea, your product or your team. I usually reply within one working day.