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 name | Purpose |
|---|---|
page.tsx | The actual page UI for this route |
layout.tsx | A wrapper around this route and all its children (shared header, sidebar, etc.) |
loading.tsx | What to show while the page is loading (React Suspense fallback) |
error.tsx | What to show if the page throws an error |
not-found.tsx | What to show for 404s within this route |
template.tsx | Like layout but re-mounts on every navigation (rarely needed) |
route.ts | A 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
awaitdata (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} /><Link> (from next/link)
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,useCallbackin 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:
| Command | What it does |
|---|---|
npm run dev | Start the dev server at http://localhost:3000. Fast refresh; reflects code changes instantly. |
npm run build | Production build. Type-checks, lints, compiles, optimizes. Run this before every push. |
npm run start | Run 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
childrenprops. 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 ontypeof window !== 'undefined'. Solution: wrap such code inuseEffect, or use a Client Component that mounts on the client. -
Environment variables and the
NEXT_PUBLIC_prefix. Variables withoutNEXT_PUBLIC_are server-only β perfect for secrets. Variables withNEXT_PUBLIC_get baked into the client bundle and are visible to the browser. Never put service_role keys or other secrets in aNEXT_PUBLIC_variable. -
localhost:3000works 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 devandnext startare different.next devis the development server with hot reload.next startruns the production build. Donβt confuselocalhost:3000fromnext devwith 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 } })orforce-dynamicexports. Those still work but are being phased out in favor of"use cache"andcacheLife. Prefer the new directive for new code. -
The App Router and Pages Router can coexist. A project can have both
app/andpages/folders. New projects should be App Router only. If youβre following an older tutorial that usespages/, 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
- React π© β the library underneath
- TypeScript π© β the type layer most Next.js projects use
- Tailwind CSS π© β the styling system most Next.js projects use
- ui π© β the component library built on top of Tailwind
- The DOM π©
- HTML π©
- JavaScript π©
- Vercel π© π¦ β where you deploy it
- Supabase π© π¦ β the database that pairs naturally with Next.js
- Environment variables π©
- Server Actions π©
- Next.js gotchas π₯
- How-to: Deploy a Next.js app to Vercel π©
- How-to: Start a new Next.js project (with the playbook) π©
- Glossary: Next.js, React, Hydration
Sources
- Next.js official docs β the canonical reference
- Next.js 16 release announcement β what changed and why
- Next.js 16.2 release notes β recent improvements (400% faster dev startup, 50% faster rendering)
- Upgrading to Next.js 16 β migration guide if coming from 15
- React 19.2 features β whatβs new in the underlying React
- App Router complete guide (2026) β third-party walkthrough with examples