Guide
Next.js fundamentals explained
React answers how to build interactive UIs; Next.js
answers how to ship them on the web at scale. Created by Vercel, Next.js is a
full-stack React framework that adds file-based routing, server rendering,
API endpoints, image optimization, and a production build pipeline out of the box.
Since Next.js 13, the App Router (the app/ directory)
is the recommended model: React Server Components stream HTML from the server,
client components hydrate only the interactive islands, and nested layouts share
chrome without re-fetching. This guide covers App Router conventions, when to mark
components "use client", data fetching and caching, route handlers,
metadata for SEO, a Harbor Supply product catalog worked example, a framework
decision table, common pitfalls, and a practitioner checklist — alongside our
React fundamentals guide,
SSR/SSG/ISR rendering guide,
and
TypeScript fundamentals guide.
What Next.js adds on top of React
A plain React single-page app (SPA) ships one HTML shell and fetches data in the browser after JavaScript loads. That pattern is fine for dashboards behind login, but public marketing pages, e-commerce catalogs, and documentation sites pay a price: slower first paint, weaker SEO, and larger client bundles. Next.js inverts the default: pages render on the server (or at build time), stream HTML to the browser, and send only the JavaScript needed for interactivity.
Core capabilities you get without assembling a custom stack:
- File-system routing — folders in
app/map to URL segments; special files (page.tsx,layout.tsx,loading.tsx,error.tsx) define UI boundaries. - Rendering modes — static generation, server rendering per request, and incremental static regeneration (ISR) via cache directives (see our rendering modes guide).
- Route handlers —
route.tsfiles expose HTTP endpoints in the same project as your UI. - Built-in optimizations —
next/imagefor responsive images,next/fontfor self-hosted web fonts, automatic code splitting per route.
Next.js is opinionated about project structure but unopinionated about data sources: REST, GraphQL, Postgres, Redis, or edge KV all plug in through async server functions and route handlers.
App Router: files, routes, and layouts
The app/ directory replaces the legacy pages/ router for
new projects. Each route segment is a folder; a page.tsx file makes
that segment publicly accessible:
app/
layout.tsx # root layout (html, body, nav)
page.tsx # /
products/
page.tsx # /products
[slug]/
page.tsx # /products/:slug
api/
health/
route.ts # GET /api/health
Layouts nest and persist
layout.tsx wraps child routes and preserves state during
client-side navigation. A root layout might render the site header; a
products/layout.tsx adds a category sidebar shared across all product
pages. Only the inner page.tsx content swaps when the user navigates
— the layout shell does not remount.
Loading and error boundaries
loading.tsx automatically wraps a route segment in React Suspense,
showing a skeleton while server data resolves. error.tsx catches
runtime errors in that segment and renders a recovery UI without crashing the
entire app. Place them at the granularity where you want isolated failure domains
(e.g. one broken product page should not blank the whole catalog).
Dynamic and catch-all segments
Bracket folders create dynamic params: [slug] for one segment,
[...slug] for catch-all, [[...slug]] for optional
catch-all. Access params in server components via the params prop
(async in Next.js 15+). Route groups (marketing) organize files
without affecting the URL.
Server Components vs client components
Every component in the App Router is a React Server Component (RSC) by default. Server components:
- Run only on the server — never ship their logic to the browser.
- Can
await fetch(), read databases, and import server-only modules directly. - Cannot use
useState,useEffect, browser APIs, or event handlers.
Add "use client" at the top of a file to opt into a
client component. Client components can use hooks and event
listeners but cannot be async functions. The recommended pattern: keep pages and
data loaders as server components; extract small interactive widgets (add-to-cart
button, modal, search autocomplete) into client children.
// app/products/[slug]/page.tsx — Server Component
export default async function ProductPage({ params }) {
const { slug } = await params
const product = await getProduct(slug)
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton sku={product.sku} />
</article>
)
}
Server components can import client components, but not vice versa. Pass serializable props (strings, numbers, plain objects) across the boundary — not functions, class instances, or Dates without serialization.
Data fetching, caching, and revalidation
In server components, use native fetch with Next.js cache extensions:
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 } // ISR: refresh at most once per hour
})
Cache behavior options:
- Default — fetch responses are cached indefinitely (static at build time when possible).
cache: 'no-store'— always fetch fresh data (dynamic rendering).next: { revalidate: N }— time-based revalidation (ISR).next: { tags: ['products'] }— on-demand revalidation viarevalidateTag('products')after a CMS publish.
For database access, call your ORM directly in server components — no API round-trip required. Use React patterns like composition to keep server data fetching at the page level and pass results down as props. For client-side mutations and optimistic UI, pair server actions or route handlers with TanStack Query in client components.
Server Actions
Server Actions are async functions marked with "use server" that run
on the server when invoked from a form or client event. They replace many ad-hoc
POST API routes for mutations (create order, update profile). Validate inputs
server-side; never trust the client.
Route handlers, middleware, and metadata
Route handlers
app/api/orders/route.ts exports HTTP method functions:
export async function POST(request: Request) {
const body = await request.json()
const order = await createOrder(body)
return Response.json(order, { status: 201 })
}
Use route handlers for webhooks, mobile app APIs, and third-party integrations. Prefer server actions for form submissions from your own UI.
Middleware
middleware.ts at the project root runs on the Edge before a request
completes. Common uses: auth redirects, A/B test bucketing, geo routing, bot
filtering. Keep middleware fast — it runs on every matched path.
Metadata API
Export a metadata object or generateMetadata function from
page.tsx / layout.tsx to set title, description, Open
Graph images, and canonical URLs per route. Dynamic product pages should call
generateMetadata with the same data source as the page body so
SEO tags stay
consistent.
Worked example: Harbor Supply product catalog
Harbor Supply sells industrial fasteners and safety equipment online. They need a fast, SEO-friendly catalog with thousands of SKUs, faceted search, and a logged-in reorder flow — without shipping a megabyte JavaScript bundle on first visit.
Route structure
app/(shop)/products/page.tsx lists categories with ISR
(revalidate: 300). app/(shop)/products/[slug]/page.tsx
renders individual SKUs from Postgres via Drizzle ORM in a server component.
app/(shop)/cart/page.tsx is a client component using Zustand for
cart state. app/api/webhooks/inventory/route.ts receives ERP stock
updates and calls revalidateTag('products').
Catalog page pattern
The products list server component queries SELECT ... LIMIT 48 with
category filters from searchParams. Product cards are server-rendered
HTML with next/image for thumbnails. A client FilterSidebar
updates URL search params via useRouter and useSearchParams;
the server component re-renders with new filters on navigation. Pagination uses
?page=2 rather than infinite scroll to keep pages crawlable.
Product detail and metadata
generateMetadata pulls SKU title, description, and OG image from the
database. Structured data (Product schema.org JSON-LD) is emitted in the server
component. The AddToCartButton client component posts to a server
action that validates stock server-side before mutating the session cart.
Deployment
Production runs next build with standalone output on a VPS behind
nginx (see our
nginx guide),
or on Vercel for zero-config edge caching. Preview deployments per pull request
let merchandising review category page changes before merge. Lighthouse targets:
LCP under 2.5s on 4G via server-rendered hero and font subsetting with
next/font.
Framework decision table
| Need | Prefer | Why |
|---|---|---|
| Full-stack React with SSR, hiring pool, Vercel deploy | Next.js App Router | Largest ecosystem; RSC + server actions reduce API boilerplate |
| Content-heavy site, minimal JS, multi-framework islands | Astro | Ships zero JS by default; embed React/Vue components where needed |
| Web standards, nested forms, progressive enhancement | Remix | Loader/action model maps cleanly to HTML forms |
| Internal admin SPA, no SEO requirement | Vite + React | Simpler mental model; no server runtime to operate |
| Approachable templates, smaller team | Vue + Nuxt | Similar file routing; gentler learning curve than React hooks |
| Real-time dashboard, WebSocket-heavy | Vite + React + dedicated API | SSR adds little value when content is auth-gated and live |
Common pitfalls
- Marking entire pages
"use client"— defeats RSC benefits; isolate interactivity. - Fetching in client
useEffecton pages that could be server-rendered — slower and worse for SEO. - Assuming
fetchis always fresh — Next.js caches aggressively; useno-storeor tags when data must be current. - Passing non-serializable props across the server/client boundary — causes runtime errors.
- Using
pages/andapp/routers mixed without a migration plan — pick App Router for greenfield. - Ignoring
loading.tsx— users see a frozen screen during slow server fetches. - Middleware auth on every static asset — scope matcher config to protected routes only.
- Deploying without testing standalone output if self-hosting — Vercel defaults differ from Docker/nginx.
Practitioner checklist
- Scaffold with
create-next-appusing App Router, TypeScript, and ESLint. - Keep data fetching in server components; reserve
"use client"for hooks and events. - Define
generateMetadataon every public page with unique titles and descriptions. - Choose cache strategy per route: static, ISR
revalidate, orno-store. - Use
next/imagewith explicit width/height to prevent layout shift. - Colocate
loading.tsxanderror.tsxat route segments with slow or fragile data. - Validate server actions and route handlers with Zod or similar; return typed errors.
- Run
next buildin CI and fail on TypeScript or lint errors. - Measure Core Web Vitals on 3G throttling after deploy, not just localhost.
- Document environment variables in
.env.example; never commit secrets.
Key takeaways
- Next.js is a full-stack React framework: routing, rendering, APIs, and optimizations in one toolchain.
- The App Router uses React Server Components by default; add
"use client"only where interactivity is required. fetchcaching andrevalidatedirectives control static, dynamic, and ISR behavior per route.- Layouts persist across navigation; loading and error files provide granular UX boundaries.
- For public catalogs and marketing sites, server-rendered Next.js beats client-only SPAs on SEO and first paint.
Related reading
- React fundamentals explained — components, hooks, and the declarative UI model Next.js builds on
- SSR, CSR, SSG and ISR explained — rendering mode trade-offs that Next.js implements natively
- TypeScript fundamentals explained — typing props, server actions, and API responses
- SEO fundamentals explained — metadata, crawlability, and Core Web Vitals for Next.js pages