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 handlersroute.ts files expose HTTP endpoints in the same project as your UI.
  • Built-in optimizationsnext/image for responsive images, next/font for 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 via revalidateTag('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 useEffect on pages that could be server-rendered — slower and worse for SEO.
  • Assuming fetch is always fresh — Next.js caches aggressively; use no-store or tags when data must be current.
  • Passing non-serializable props across the server/client boundary — causes runtime errors.
  • Using pages/ and app/ 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-app using App Router, TypeScript, and ESLint.
  • Keep data fetching in server components; reserve "use client" for hooks and events.
  • Define generateMetadata on every public page with unique titles and descriptions.
  • Choose cache strategy per route: static, ISR revalidate, or no-store.
  • Use next/image with explicit width/height to prevent layout shift.
  • Colocate loading.tsx and error.tsx at route segments with slow or fragile data.
  • Validate server actions and route handlers with Zod or similar; return typed errors.
  • Run next build in 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.
  • fetch caching and revalidate directives 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