Next.js development

Next.js is the React framework we reach for when a product needs to be indexable and interactive at the same time. The page you are reading is a Next.js 14 App Router build on Vercel, so most of what follows is written from operating it, not from reading the docs.

What Next.js is

Next.js is Vercel's React framework. It adds file-based routing, React Server Components, per-route rendering control (static, incrementally revalidated, or dynamic), route handlers for HTTP endpoints, and a build pipeline that code-splits and optimises images and fonts without configuration. The defining idea is that rendering is a per-route decision rather than an application-wide one — the same codebase can serve a statically generated marketing page and a request-time authenticated dashboard.

Prefer the short definition? Read the Next.js glossary entry.

The honest version

When Next.js is the right choice — and when it is not

Choosing a technology because it is popular is how projects end up expensive. Here is where we would pick it, and where we would tell you not to.

Good fit

A product with both a public marketing surface and a logged-in application
This is the case Next.js wins outright. Marketing routes render statically or on a revalidation interval and are crawlable; app routes render per-request behind auth. One repo, one design system, one deploy — instead of a WordPress site bolted next to a separate React SPA, with two build pipelines and two sets of headers to keep consistent.
Content that changes on a publisher's schedule, not a user's — storefronts, catalogues, documentation, listings
Incremental Static Regeneration plus tag-based invalidation lets a CMS or PIM webhook revalidate exactly the affected routes. Readers get cached HTML at CDN latency; editors do not wait for a full rebuild. Our headless commerce work at /case-studies/ecommerce-headless-commerce was built on this shape — storefront pages served from cache, invalidated on catalogue change.
SEO is a real acquisition channel and the UI is genuinely interactive
Server-rendered HTML with correct metadata, canonical URLs, and structured data arrives in the first response, and hydration adds interactivity afterwards. Client-side-only React makes you choose between the two or run a prerendering service alongside.
An early-stage product where one small team owns the UI and the thin API layer in front of it
Route handlers let the same engineers ship the screen and the endpoint it calls without a separate service, a second deployment target, or CORS. That collapses a lot of coordination cost early on — see /services/mvp-development for how we scope that.

We would choose something else

A pure app-shell product behind a login with no SEO surface at all — an internal admin tool, a trading terminal, a back-office console
You pay the entire cost of the server/client boundary and get nothing back. Every interactive component needs a "use client" boundary, every prop crossing that boundary must be serialisable, and you inherit four interacting caches you did not ask for. A Vite plus React SPA served as static files with a real API behind it is less code, faster to build, and easier for a new engineer to reason about. We recommend that regularly.
A team already committed to running on its own infrastructure — on-premise, a regulated VPC, or bare Kubernetes
Next.js self-hosts, but the framework's happy path is Vercel and the gap is real. ISR and on-demand revalidation need a shared cache handler once you run more than one replica, otherwise instances serve divergent cached HTML. The built-in image optimiser wants persistent cache and CPU you now budget for. Middleware runs in-process on a self-hosted server rather than at a CDN edge, so the latency argument for it disappears and every request pays for it on your own hardware. None of it is impossible — it is infrastructure engineering you are funding to reproduce behaviour that is free on the platform Next.js is built to sell.
A near-static content site — brochureware, documentation, a blog with little interactivity
Next.js ships a React runtime and hydrates the page whether or not anything on it moves. Astro, Eleventy, or Hugo emit the same HTML with a fraction of the JavaScript and no hydration step. Choosing Next.js here means shipping a framework to render text.
Compute-heavy, long-running, or connection-holding backend work placed in route handlers
Serverless functions have execution ceilings, cold starts, and no durable process. Video transcoding, multi-minute report generation, a WebSocket server, or a job that must survive a deploy do not belong there. Database connections are the sharper edge: every concurrent invocation opens its own pool, so Postgres hits max_connections under load unless PgBouncer or a serverless driver sits in front of it. That work belongs in a dedicated service — see /services/api-development.
A large existing React SPA where the ask is "add SSR"
An App Router migration is not a rendering flag, it is a rearchitecture. Context providers, browser-only libraries, `window` access at module scope, and client-owned global state all sit on the wrong side of the boundary and have to be moved or wrapped. Add the App Router's own churn — caching defaults have shifted meaningfully between major versions — and the honest estimate is a rewrite of the data layer. Often the better answer is prerendering the handful of routes that need to rank, or treating it as /services/software-modernization scoped as a deliberate migration rather than an upgrade.

In practice

What we build with Next.js

  • Marketing and content surfaces that have to rank — statically generated or revalidated on an interval, with metadata, canonical URLs, sitemap and structured data generated from the same content source as the page, so they cannot drift apart.
  • Headless storefronts and catalogue sites where a CMS, PIM, or commerce API is the source of truth and cache invalidation is driven by webhook rather than by rebuild.
  • Authenticated dashboards and SaaS front ends where session-scoped data renders per-request on the server, and the client bundle carries only the components that genuinely need interactivity.
  • Backend-for-frontend layers in route handlers: aggregating several upstream services into one response the UI actually wants, holding API keys server-side, and applying rate limiting at the edge of the app.
  • Migrations from Pages Router to App Router, run route by route with the two routers coexisting, rather than as a stop-the-world rewrite.

Positions

Decisions we have already made

Defaults we start from. They are arguable — but they are argued, not assumed, and we will change them for a good reason.

Rendering: static or revalidated by default, dynamic only where the response genuinely depends on the request
We set an explicit rendering mode on every route instead of letting one accidental dynamic API call opt a whole subtree into per-request rendering. This site's homepage carries `export const revalidate = 3600` — the content changes on our schedule, not the visitor's, so it is served from cache and refreshed hourly. Dynamic rendering is reserved for routes reading cookies, headers, or user-scoped data. The failure mode we design against is the page that could have been cached being silently rendered per request because something four levels down called `headers()`.
State: the URL is the store; a client state library is the exception
Filters, tabs, pagination and sort belong in search params — shareable, bookmarkable, survive reload, and let the server render the correct view directly. Server data stays in server components rather than being re-fetched on the client. We add TanStack Query only where there is genuine client-side cache coherence to manage (optimistic writes, polling, offline), and a global store like Zustand or Redux only for state the client truly owns — an editor's undo stack, a canvas, a multi-step form in flight. Reaching for Redux on day one of a Next.js app usually means the server/client boundary was drawn in the wrong place.
Data access: a pooled connection and a typed query layer, never a fresh pool per invocation
On serverless we put PgBouncer or a provider's pooled endpoint between the app and Postgres, because each concurrent function instance otherwise opens its own connections and exhausts the server. For the query layer we default to Drizzle — the generated SQL is predictable and migrations are plain SQL files we can read in review. Prisma is a reasonable choice on a long-lived Node server; on serverless its engine adds cold-start weight for a benefit a typed query builder already provides. Either way, anything that is not a straightforward read stays in a real service rather than growing inside a route handler.
Deployment: Vercel when the coupling is acceptable, containerised `next start` when it is not — and the code should not care which
Vercel is the fastest correct path and what this site runs on. But we keep the application portable by default: no platform-specific primitives in business logic, middleware kept to cheap work like redirects and header rewrites rather than becoming an auth tier, and the Node runtime preferred over the edge runtime unless a route measurably benefits — the edge runtime is a restricted API surface, not a faster version of Node. Here, `runtime = "edge"` is used only for the OG image and icon routes, where the workload actually suits it. If a client needs to move to their own cluster later, that should be an infrastructure project, not a rewrite.

Architecture

Things worth getting right early

The server/client boundary is the main design artefact
Where you place "use client" determines your bundle, your data flow, and how much of React ships to the browser. We push the boundary down to the leaves — an interactive filter widget is a client component, the page containing it is not — rather than marking a layout as client and dragging its whole subtree along. Everything crossing the boundary must serialise: no class instances, no functions, no Date inside a Map. The practical discipline is to treat each client component as a module with an explicit props contract, the way you would a service boundary.
Caching is four layers, and the defaults will surprise you
Request memoisation, the Data Cache, the Full Route Cache, and the client Router Cache each have their own lifetime and their own invalidation. Most Next.js bugs that reach production are one of these behaving exactly as documented while the team assumed something else. We make cache intent explicit per fetch, use tag-based invalidation so a content change revalidates the routes that depend on it and nothing more, and pin the Next.js minor version — caching defaults have changed between releases often enough that an unattended upgrade is a behavioural change, not a patch.
Route handlers are a backend-for-frontend, not the system of record
They aggregate, shape, and guard: call two or three upstream services, drop the fields the UI does not need, keep credentials server-side, apply rate limiting. Business rules, background jobs, and anything transactional live in a service designed for it — Node, Go, or Python — where you control the runtime, the connection lifecycle, and retry semantics. The API routes on this site are all in that thin category: form submission, feedback, newsletter, lead capture. When a route handler starts growing a domain model, that is the signal to extract it.
Performance work is bundle discipline first, then streaming
Server components remove code from the client for free; after that it is measurement. On this site, animation and icon libraries are the two largest client-side costs, so barrel imports are rewritten to per-symbol paths at build time via `optimizePackageImports` and animation features are loaded lazily rather than in the initial chunk. Suspense boundaries go where a slow upstream call would otherwise hold the entire response — the shell streams, the slow region fills in. We instrument with OpenTelemetry through the framework's instrumentation hook so server render time, data fetches, and route handlers appear as spans rather than one opaque duration.

Around it

What Next.js usually sits next to

  • React

    Next.js is a React framework — Server Components, Suspense and the concurrent renderer are React features that Next.js routes around.

  • TypeScript

    Strict mode by default. The props crossing every server/client boundary are a contract, and the compiler is what keeps that contract honest through refactors.

  • Node.js

    The runtime under route handlers and server rendering, and the target when the app is containerised with `next start` instead of deployed to a serverless platform.

  • PostgreSQL

    The usual system of record behind a Next.js app — with a pooled connection in front of it, because serverless invocation counts and Postgres connection limits do not get along by default.

  • Redis

    Shared state a stateless function tier cannot hold: rate-limit counters, session lookups, and the shared cache handler that makes ISR behave consistently across self-hosted replicas.

  • CI/CD

    Preview deployments per pull request are what make Next.js pleasant to review — the change is a URL a stakeholder can open, not a screenshot.

Questions

Common questions about Next.js

  • Do we have to deploy on Vercel?

    No. Next.js runs as a Node server in a container behind any CDN, and we build applications so that path stays open — Node runtime by default, middleware kept thin, no platform-specific primitives in business logic. What you should know going in is that self-hosting means owning the parts Vercel provides: a shared cache handler so ISR is consistent across replicas, image optimisation compute and storage, and your own edge tier for redirects and header rewrites. If you plan to self-host from day one, tell us at the start — it changes several design decisions, not just the deploy script.

  • App Router or Pages Router for a new project?

    App Router. Pages Router still works and is not disappearing quickly, but new framework capability lands in the App Router and the ecosystem has moved. The caveat is that the App Router asks more of the team: the server/client split, the caching model, and the fact that its defaults have shifted across major versions are all real learning cost. For an existing Pages Router application, the two routers coexist in one app, so migration is route by route rather than all at once.

  • Is Next.js a backend framework? Can it replace our API?

    It can hold a backend-for-frontend layer, and for a small product that is often enough. It is not where a domain backend should live. Serverless functions have execution ceilings, cold starts, no durable process, and a connection model that fights connection-pooled databases. Background jobs, scheduled work, long-lived connections, and anything transactional belong in a service built for it. The clean split is: route handlers aggregate and guard, a real service owns the domain.

  • Will Next.js make our site fast?

    It removes some obstacles and introduces others. Server components and static rendering genuinely reduce what ships to the browser, and image and font handling avoid common layout-shift mistakes. But a Next.js app can be slow — a client component near the root of the tree, a heavy animation or icon library imported through a barrel file, or an uncached upstream call blocking the response will each undo the benefit. Speed comes from measuring the specific application, not from the framework choice.

  • How do we know you have actually run this in production?

    This site is a Next.js 14 App Router build on Vercel. The homepage is revalidated on an interval, the edge runtime is used only where it suits the workload, barrel imports are rewritten at build time to keep route chunks small, and the Content-Security-Policy was deployed report-only first because a client-side animation library emits inline styles a strict policy would block. Those are the kinds of details you only accumulate by shipping and then fixing what the numbers show.

Services that use Next.js

Where it lands

Tell us what the app has to do, and we will tell you whether Next.js is the right tool

If your product has a public surface that must rank and an application behind a login, Next.js is probably the answer and we can scope it. If it is an internal console with no SEO surface, or you need to run on your own cluster, we will say so and propose something simpler. Send us the shape of the product — the routes, the data sources, where it has to run — and you will get a straight technical read before any pricing conversation.