Next.js

Status: 🟩 COMPLETE (🟦 LIVING β€” Next.js evolves fast) Last updated: 2026-06-20 (current major version: Next.js 16) Plain-English tagline: The React framework that handles routing, server rendering, data fetching, and deployment so you can build a whole webapp instead of just a UI.


In plain English

React on its own is a library for building user interfaces β€” it gives you components and state, and that’s about it. To actually build a real web app you also need: a way to organize pages, a way to fetch data from a server, a way to handle URLs, a way to serve images efficiently, a way to render HTML on the server for fast page loads, and a way to package it all up for deployment. Next.js is the framework that provides all of that, on top of React.

Think of React as the engine and Next.js as the rest of the car β€” chassis, wheels, steering, dashboard. You could build a car around just an engine, but you’d be reinventing a lot. Next.js is the highly-opinionated, batteries-included car everyone agrees works well.

Next.js is made by Vercel (the company also makes the hosting platform Vercel β€” see Vercel). It is open source under the MIT license. As of 2026 it is on version 16 and the App Router is the default and recommended approach for all new projects.


Why it matters

Most modern React-based webapps you’ve ever used were built with Next.js: Notion’s docs, ChatGPT’s website, the Anthropic site, TikTok’s web interface, hundreds of e-commerce stores, dashboards, blogs, marketing sites. It is the default React framework in 2026 by a wide margin.

If you’re going to learn one framework deeply, this is the one. The skills transfer to nearly every modern React role. And for solo developers, the Next.js + Vercel + Supabase stack (the one St Mark’s Bible Quest is built on) is the fastest, cheapest way to ship a real webapp by yourself.


A brief history (so it makes sense)

Next.js has had two major routing systems:

  • Pages Router (Next.js 1–12, still supported): files in pages/ map to URLs. pages/about.js β†’ /about. Simple, well-understood, mostly stable. Many older tutorials use this.
  • App Router (Next.js 13+, default since Next 13): files in app/ map to URLs, with a much richer system involving layouts, loading states, error boundaries, and React Server Components. More powerful, takes more learning, but is the future.

You should learn the App Router. It’s what every new project uses. The Pages Router still exists for legacy reasons but is essentially in maintenance mode.

This entry focuses entirely on the App Router.


How the App Router works

File-based routing

In the App Router, the folder structure under app/ IS your URL structure. A file called page.tsx in a folder turns that folder’s path into a route.

app/
β”œβ”€β”€ page.tsx                  β†’ /
β”œβ”€β”€ about/
β”‚   └── page.tsx              β†’ /about
β”œβ”€β”€ blog/
β”‚   β”œβ”€β”€ page.tsx              β†’ /blog
β”‚   └── [slug]/
β”‚       └── page.tsx          β†’ /blog/:anything
└── (marketing)/
    └── pricing/
        └── page.tsx          β†’ /pricing  (the (marketing) folder doesn't appear in the URL)

Special file names have special meanings:

File namePurpose
page.tsxThe actual page UI for this route
layout.tsxA wrapper around this route and all its children (shared header, sidebar, etc.)
loading.tsxWhat to show while the page is loading (React Suspense fallback)
error.tsxWhat to show if the page throws an error
not-found.tsxWhat to show for 404s within this route
template.tsxLike layout but re-mounts on every navigation (rarely needed)
route.tsA server-side API endpoint (no UI)
middleware.ts(at project root) runs before every request

Anything inside [brackets] becomes a dynamic parameter β€” [slug] matches anything in that URL segment. [[...catchall]] matches multiple segments. (parentheses) group folders without affecting the URL.

Server Components vs Client Components

This is the single most important concept in the App Router, and the one that confuses everyone at first.

  • Server Components (the default) β€” run only on the server. Can await data (database queries, API calls), can read environment variables including secrets, never ship JavaScript to the browser. The HTML they produce is sent to the browser and that’s it.
  • Client Components β€” run in the browser. Can use React state (useState), effects (useEffect), event handlers (onClick), and browser APIs. To mark a component as a Client Component, put "use client" at the very top of the file.
// app/page.tsx β€” Server Component (default)
async function HomePage() {
  const users = await db.from("users").select("*"); // runs on the server
  return <UserList users={users} />;
}
 
// app/UserList.tsx β€” Client Component
"use client";
import { useState } from "react";
export function UserList({ users }) {
  const [filter, setFilter] = useState("");
  return (
    <div>
      <input value={filter} onChange={(e) => setFilter(e.target.value)} />
      {users.filter(u => u.name.includes(filter)).map(...)}
    </div>
  );
}

The Server Component fetches data on the server (no API endpoint needed!), then passes the data as props to the Client Component, which handles the interactive bits.

Rule of thumb: Default to Server Components. Mark something "use client" only when you need state, effects, event handlers, or browser APIs.

Layouts β€” UI that wraps multiple pages

A layout.tsx wraps every page in its folder (and subfolders):

// app/layout.tsx β€” wraps every page in the app
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  );
}
 
// app/dashboard/layout.tsx β€” wraps every /dashboard/* page
export default function DashboardLayout({ children }) {
  return (
    <div className="flex">
      <Sidebar />
      <main>{children}</main>
    </div>
  );
}

The RootLayout at app/layout.tsx is required β€” it wraps your entire app. It must include <html> and <body> tags.

Loading states

Drop a loading.tsx file in any folder, and Next.js automatically uses it as the React Suspense fallback while that page’s data is loading:

// app/blog/loading.tsx
export default function Loading() {
  return <div className="animate-pulse">Loading posts…</div>;
}

Combined with async Server Components, this gives you streaming pages β€” the layout loads instantly, and content streams in as data arrives. No spinner-state-management code required.

Data fetching

Server Components can fetch data directly β€” no API layer required:

// app/blog/page.tsx
async function BlogPage() {
  const posts = await fetch("https://api.example.com/posts").then(r => r.json());
  // OR
  const posts = await db.from("posts").select("*");
 
  return posts.map(post => <PostCard key={post.id} post={post} />);
}

For data fetching with explicit caching control (new in Next 16), use the "use cache" directive (covered in the Caching section below).

Server Actions β€” backend functions you call like JavaScript functions

Want to handle a form submission, or insert a row in the database? Don’t build an API endpoint β€” write a Server Action:

// app/posts/new/page.tsx
async function createPost(formData: FormData) {
  "use server"; // this marks it as a Server Action β€” only runs on the server
  const title = formData.get("title") as string;
  await db.from("posts").insert({ title });
  revalidatePath("/blog");
}
 
export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">Create</button>
    </form>
  );
}

When the form submits, Next.js automatically POSTs to the server and runs createPost. No fetch, no API route, no JSON wrangling. It feels almost too easy, but it works.

Route handlers β€” when you actually do need an API endpoint

For things that aren’t form submissions (webhooks, OAuth callbacks, third-party API access, etc.), define a route.ts:

// app/api/webhook/route.ts
export async function POST(request: Request) {
  const body = await request.json();
  // process the webhook
  return Response.json({ ok: true });
}

The file exports functions named after HTTP methods (GET, POST, PUT, DELETE, PATCH).


Caching in Next.js 16 β€” the new "use cache" directive

Earlier versions of the App Router had implicit caching β€” Next.js cached things automatically based on heuristics, and it was confusing when caching kicked in and when it didn’t. Next.js 16 made caching explicit and opt-in via the "use cache" directive.

// Cache a whole function's result
async function getProducts() {
  "use cache";
  return await db.from("products").select("*");
}
 
// Cache an entire page
"use cache";
export default async function ProductsPage() {
  const products = await getProducts();
  return <ProductList products={products} />;
}

You can set how long the cache lives:

import { unstable_cacheLife as cacheLife } from "next/cache";
 
async function getDailyReport() {
  "use cache";
  cacheLife("hours");  // refresh hourly
  return await db.from("reports").select("*");
}

This is much clearer than the old β€œI think this is cached but I’m not sure” experience. If a function doesn’t have "use cache", it’s not cached. Simple.


Built-in components and helpers worth knowing

Next.js ships several components that improve on plain HTML equivalents:

<Image> (from next/image)

Replacement for <img>. Automatically optimizes images (resizes, converts to WebP/AVIF, serves the right size for the user’s device), lazy loads them, and prevents layout shift.

import Image from "next/image";
 
<Image src="/cat.jpg" alt="My cat" width={500} height={300} />

Replacement for <a> for internal navigation. Prefetches the destination page in the background; navigates instantly without a full page reload.

import Link from "next/link";
 
<Link href="/about">About</Link>

<Script> (from next/script)

For loading external JavaScript with control over when it loads (before page is interactive vs after vs lazy).

Fonts (from next/font)

Self-hosts Google Fonts at build time β€” no external request to fonts.googleapis.com, no layout shift, no Cumulative Layout Shift penalty.

import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
// then use inter.className

<Metadata> API

Define <title>, <meta>, Open Graph tags, etc. by exporting a metadata object from a page or layout:

export const metadata = {
  title: "My Blog",
  description: "A blog about cats",
  openGraph: { images: ["/og.jpg"] }
};

React 19.2 + the React Compiler

Next.js 16 ships with React 19.2 and stable React Compiler 1.0 support.

The big practical wins for you:

  • React Compiler automatically memoizes your components and hooks. You no longer need to manually use React.memo, useMemo, useCallback in most cases β€” the compiler figures out what needs caching and inserts it. Less boilerplate, fewer re-render bugs.
  • <ViewTransition> β€” wraps content for smooth animated transitions between routes. (Powered by the browser’s View Transitions API.)
  • useEffectEvent β€” extract a function from an effect that always sees fresh state without triggering re-runs. Fixes a long-standing footgun.
  • Activity β€” render content invisibly in the background (preloading, etc.) without triggering effects.

You don’t need to use any of these manually β€” they’re just available when you want them.


The build process

Three commands you’ll use constantly:

CommandWhat it does
npm run devStart the dev server at http://localhost:3000. Fast refresh; reflects code changes instantly.
npm run buildProduction build. Type-checks, lints, compiles, optimizes. Run this before every push.
npm run startRun the production build locally (after npm run build).

Next.js 16’s Turbopack (the bundler) makes npm run dev start ~400% faster than Next.js 15 and rendering ~50% faster. Builds are also significantly faster.


Project structure (typical)

my-nextjs-app/
β”œβ”€β”€ app/                    # App Router pages, layouts, routes
β”‚   β”œβ”€β”€ layout.tsx
β”‚   β”œβ”€β”€ page.tsx
β”‚   └── ...
β”œβ”€β”€ components/             # Reusable React components
β”œβ”€β”€ lib/                    # Helper functions (db client, utils, etc.)
β”œβ”€β”€ public/                 # Static files served as-is at the root URL
β”œβ”€β”€ styles/                 # Global CSS (if not using Tailwind alone)
β”œβ”€β”€ .env.local              # Local environment variables (gitignored)
β”œβ”€β”€ next.config.js          # Next.js configuration
β”œβ”€β”€ package.json            # Dependencies and scripts
β”œβ”€β”€ tsconfig.json           # TypeScript configuration
└── tailwind.config.ts      # Tailwind configuration (if using Tailwind)

This is a convention, not a hard rule β€” you can organize differently β€” but virtually every Next.js project looks roughly like this.


A concrete example: a tiny but realistic page

A blog post page that fetches data on the server and renders it:

// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
 
// Generate static metadata (for SEO + sharing)
export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return {
    title: post?.title ?? "Not found",
    description: post?.excerpt,
  };
}
 
// Fetch the post data (server-side)
async function getPost(slug: string) {
  "use cache";
  return await db.from("posts").select("*").eq("slug", slug).single();
}
 
// The page itself
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  if (!post) notFound(); // renders the nearest not-found.tsx
 
  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.publishedAt}</time>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  );
}

That’s the entire blog post page β€” no API route, no client-side fetch, no loading spinner code. The data is fetched on the server, the cache directive makes it efficient, the metadata is auto-generated for SEO, 404s are handled by the framework.


Common gotchas

  • β€œuse client” must be the very first line. Even comments above it move it from β€œthe first thing in the file” and Next.js stops treating the file as a Client Component. Put it at line 1.

  • Server Components can’t use hooks. useState, useEffect, useRef, etc. only work in Client Components. If you need them, mark the file "use client".

  • Client Components can import Server Components, but only as children props. You can’t directly call a Server Component from inside a Client Component β€” pass it through as a prop instead. This is awkward but works.

  • Server Components can’t pass functions as props to Client Components. Functions can’t be serialized across the server/client boundary. If you need to pass a callback, that callback has to be defined in the Client Component itself or wrapped in a Server Action.

  • 'use server' and "use client" are completely different. "use client" marks a component as client-side. 'use server' (note: typically with single quotes by convention) marks a function as a Server Action that can be called from the client. Easy to mix up.

  • Hydration mismatches. If the HTML the server rendered doesn’t exactly match what React expects on the client, you get a β€œHydration failed” error. Common causes: Date.now(), Math.random(), browser-only APIs (window, localStorage), or conditionally rendering based on typeof window !== 'undefined'. Solution: wrap such code in useEffect, or use a Client Component that mounts on the client.

  • Environment variables and the NEXT_PUBLIC_ prefix. Variables without NEXT_PUBLIC_ are server-only β€” perfect for secrets. Variables with NEXT_PUBLIC_ get baked into the client bundle and are visible to the browser. Never put service_role keys or other secrets in a NEXT_PUBLIC_ variable.

  • localhost:3000 works but the production build fails. Almost always one of: TypeScript error, missing env var on Vercel, or case-sensitivity (Vercel runs Linux which is case-sensitive). See Deploy a Next.js app to Vercel.

  • next dev and next start are different. next dev is the development server with hot reload. next start runs the production build. Don’t confuse localhost:3000 from next dev with how the production version actually behaves β€” they differ in subtle ways (caching, certain optimizations, error boundaries).

  • Caching changes in Next.js 16. Older tutorials may show fetch(url, { next: { revalidate: 60 } }) or force-dynamic exports. Those still work but are being phased out in favor of "use cache" and cacheLife. Prefer the new directive for new code.

  • The App Router and Pages Router can coexist. A project can have both app/ and pages/ folders. New projects should be App Router only. If you’re following an older tutorial that uses pages/, mentally translate or find an App Router version.


When NOT to use Next.js

Next.js is the right choice for most webapps, but not all:

  • Pure static blogs / docs: Astro is often a better fit β€” smaller, faster, more flexible for content-heavy sites.
  • Static landing pages: Astro again, or plain HTML/CSS.
  • Native mobile apps: React Native (different framework, shares the React knowledge).
  • Embedded React widgets in non-React sites: Plain React via Vite is lighter.
  • Heavy use of obscure web platform features: You may butt up against Next.js’s opinions.

For everything else β€” dashboards, SaaS apps, e-commerce, marketing sites with interactivity, social apps β€” Next.js is the default and a very safe choice.


See also


Sources