Getting Started
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Next.js 16.3.8 (the latest release on 2026-10-03, when the facts and examples below were
checked; the official documentation reports @doc-version 16.3.8), React 19.3.0, Node.js 20.9.0 or later and
TypeScript 5.1 or later. It covers the App Router, which is what create-next-app generates, and it shows the
Pages Router equivalent wherever the setup differs. Caching is only touched where it changes the first pages you
write: the examples use the previous model (the default, in which nothing is cached unless you ask), and one
section shows the same page under the opt-in Cache Components model (cacheComponents: true). Every example was
built and run with Next.js 16.3.8, Node.js 24.16 and npm 11.13 on macOS, in a scratch copy of the Bookshelf — the
catalogue of books, authors and reviews that the rest of this section grows, and that the
Django and NestJS sections also build as a
back end — and each one is followed by the official page it derives from.
The page starts at an empty folder and ends with a running, type-checked, linted and production-built application, a working dev loop, and a map of the official getting-started sequence. It does not re-teach React; if components, props and state are new, read React Getting Started first, and Server Rendering, SSG and React Server Components for the ideas behind the rendering modes that Next.js automates.
System Requirements
Next.js is a Node.js program: the dev server, the build and next start all run on it. The requirements below are the
ones on the official installation page for 16.3.8.
| Requirement | Value | Notes |
|---|---|---|
Node.js |
20.9.0 or later |
This is also the |
Operating systems |
macOS, Windows (including WSL) and Linux |
Turbopack ships native bindings for macOS (x64, ARM64), Windows (x64, ARM64) and Linux glibc and musl (x64,
ARM64). Elsewhere (FreeBSD, OpenBSD) Next.js falls back to WebAssembly bindings, which do not support Turbopack, and
you must pass |
Browsers |
Chrome 111+, Edge 111+, Firefox 111+, Safari 16.4+ |
Modern browsers need no configuration. Polyfills and browser targets are described in the official browser-support page. |
TypeScript |
5.1.0 or later (optional) |
Next.js 16.3 can also use TypeScript 7 for the type check in |
React |
|
The App Router bundles its own React canary, which contains the stable React 19 features plus newer ones; you still
declare |
Package manager |
npm, pnpm, Yarn or Bun |
|
Check the Node.js version before anything else; most "it does not start" reports in a new project are an old Node.js.
node --version # must be v20.9.0 or later
npm --version
npx next --version # inside a project: prints the installed Next.js
Creating a Project with create-next-app
The quickest start is create-next-app, which writes
the folder layout, installs the dependencies and configures TypeScript, the linter, the CSS toolchain, the import
alias and the agent guidance file. --yes skips every prompt and uses your saved preferences or the defaults.
npx create-next-app@latest bookshelf --yes
cd bookshelf
npm run dev
Source: Installation: quick start
Run without arguments, the CLI asks interactively. The first prompt names the project; the second chooses between the recommended defaults, your previous settings, or a customised set. The prompts below are the ones documented for Next.js 16.3.8.
What is your project named? my-app
Would you like to use the recommended Next.js defaults?
Yes, use recommended defaults - TypeScript, ESLint, Tailwind CSS, App Router, AGENTS.md
No, reuse previous settings
No, customize settings - Choose your own preferences
# If you choose "customize settings":
Would you like to use TypeScript? No / Yes
Which linter would you like to use? ESLint / Biome / None
Would you like to use React Compiler? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like your code inside a `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the import alias (`@/*` by default)? No / Yes
What import alias would you like configured? @/*
Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code? No / Yes
Every answer has a flag, so a project can be created non-interactively and reproducibly in a script or in CI.
| Option | What it does | Default |
|---|---|---|
|
TypeScript or JavaScript project. |
TypeScript |
|
ESLint (with |
ESLint |
|
Tailwind CSS configuration. Without it you get a plain CSS module ( |
Tailwind CSS v4 |
|
Adds |
Off |
|
App Router, or the Pages Router ( |
App Router |
|
Puts application code in |
No |
|
The |
|
|
A headless project with only Route Handlers (no layout and no page). |
Off |
|
A minimal page and layout: no starter content and no |
Off |
|
Written into the generated scripts. Turbopack is the default bundler. |
Turbopack |
|
Includes (or omits) |
Included |
|
Chooses the package manager to bootstrap with. |
Detected from how it was invoked |
|
Bootstraps from an official example or from any public GitHub repository. |
None |
|
Skips installing packages, or skips |
Install and |
|
Uses saved preferences or the defaults for anything not given; or forgets the saved preferences. |
Prompts |
Source: create-next-app: reference
The table follows the official reference page. npx create-next-app@latest --help on 16.3.8 additionally lists
--rspack (an experimental Rspack bundler option) and does not print --turbopack, --webpack, --no-linter or
--no-app, so treat --help as the authority for what your installed version accepts.
What the defaults generate
The recommended defaults were run with npx create-next-app@latest bookshelf --yes --use-npm --disable-git. The tool
installed the three runtime packages and eight development packages, generated the route types, and wrote this tree:
AGENTS.md)bookshelf/
├── AGENTS.md guidance for coding agents (points at the docs bundled in node_modules)
├── CLAUDE.md one line: @AGENTS.md
├── README.md
├── app/
│ ├── favicon.ico
│ ├── globals.css Tailwind v4 entry: @import "tailwindcss"
│ ├── layout.tsx root layout (required)
│ └── page.tsx the home page, route /
├── eslint.config.mjs ESLint flat config
├── next-env.d.ts generated: makes the Next.js types visible to TypeScript
├── next.config.ts empty NextConfig
├── package.json
├── package-lock.json
├── postcss.config.mjs @tailwindcss/postcss
├── public/ five starter SVGs, served from /
└── tsconfig.json strict, "paths": { "@/*": ["./*"] }
Source: Project structure
The generated package.json is short. Note the exact versions: next and eslint-config-next are pinned to the
version of the tool, and react is pinned to the React release the tool was published with (19.2.8 for
create-next-app 16.3.8, although react 19.3.0 was already on npm; upgrade it with the command in the upgrading
section below).
{
"name": "bookshelf",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"next": "16.3.8",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.3.8",
"tailwindcss": "^4",
"typescript": "^5"
}
}
The tsconfig.json enables the Next.js TypeScript plugin and the @/* import alias, and includes the generated route
types, so PageProps and LayoutProps are available without imports. The alias maps @/lib/books to lib/books at
the project root (or to src/lib/books when you chose --src-dir).
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"strict": true,
"noEmit": true,
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./*"] }
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}
Source: TypeScript
The ESLint setup is a flat config that extends the Next.js rule sets. Since Next.js 16 the linter is run by its own
CLI (eslint), not by next lint.
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
Source: ESLint
Three details of the scaffold matter later. First, .gitignore contains .env*, so environment files are not
committed by default (the official environment-variables guide warns that you almost never want to commit them).
Second, next-env.d.ts is listed in .gitignore, because Next.js regenerates it. Third, the starter layout.tsx loads
the Geist fonts through next/font/google; the font files are fetched when the project is built, so the first build
needs network access (self-hosting the font files is covered with fonts in the official sequence listed below).
The other flavours, as generated
Each flavour below was generated with --skip-install --disable-git and its files and package versions were
inspected. The default project above, which is the base of the Bookshelf in the next sections, was also built,
type-checked and linted.
| Command | Result |
|---|---|
|
|
|
No ESLint: |
|
|
|
Only |
|
A minimal |
|
Adds |
Source: create-next-app: examples and reactCompiler
The generated --api template is a good minimal Route Handler. The second handler reads a dynamic segment, and since
Next.js 15 params is a Promise that must be awaited.
// app/route.ts
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ message: "Hello world!" });
}
// app/[slug]/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> },
) {
const { slug } = await params;
return NextResponse.json({ message: `Hello ${slug}!` });
}
Source: route.js
An example from the official repository, or from any public GitHub repository, replaces the template altogether:
npx create-next-app@latest --example [example-name] bookshelf
npx create-next-app@latest --example "https://github.com/<owner>/<repo>" bookshelf
The AGENTS.md and CLAUDE.md files
Since 16.2, create-next-app includes AGENTS.md by default (--no-agents-md opts out), together with a one-line
CLAUDE.md containing @AGENTS.md. The file does not teach anything itself: it tells a coding agent to read the
documentation that ships inside the next package, in node_modules/next/dist/docs/, which always matches the
installed version, instead of relying on training data. A human reader gets the same benefit, because those files are
the official docs as Markdown.
next dev and create-next-app maintain in AGENTS.md<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules -->
Source: AI coding agents
From 16.3, next dev also writes and maintains this block in existing projects when it detects an AI coding agent in
the environment (it was observed in the manual-installation run below, where the log printed that it had generated
AGENTS.md and CLAUDE.md). Content you write outside the BEGIN and END markers is preserved. To opt out of the
automatic generation, set agentRules to false:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
agentRules: false,
}
export default nextConfig
Source: AI coding agents: opting out
On 16.2 the docs are bundled but the file is not auto-generated; on 16.1 and earlier the docs are not bundled and the
legacy npx @next/codemod@canary agents-md command downloads a version-matched copy into .next-docs/.
Manual Installation
Nothing in Next.js requires create-next-app: it is a convenience. A project is three packages, three scripts and
two files. This sequence was run from an empty folder, with the file contents exactly as shown.
mkdir bookshelf && cd bookshelf
npm init -y
npm i next@latest react@latest react-dom@latest
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"lint:fix": "eslint --fix"
}
}
The app directory needs one file, the root layout, which must render the <html> and <body> tags; the home page
is optional but nothing would be served at / without it.
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
// app/page.tsx
export default function Page() {
return <h1>Hello, Next.js!</h1>
}
Start npm run dev once. Because the project contains TypeScript files and no TypeScript, Next.js installs the
missing packages and writes a tsconfig.json for you. The first start, as logged:
npm run dev of a manual installation (trimmed)▲ Next.js 16.3.8 (Turbopack)
- Local: http://localhost:3000
✓ Ready in 157ms
It looks like you're trying to use TypeScript but do not have the required package(s) installed.
Installing devDependencies (npm):
- typescript
- @types/react
- @types/node
We detected TypeScript in your project and created a tsconfig.json file for you.
✓ Generated AGENTS.md and CLAUDE.md for AI agents. Set `agentRules: false` in next.config to disable.
Source: Installation: set up TypeScript
Two things differ from the scaffolded project. The auto-installed typescript is the newest release (7.0.2 on the
checked date), whereas the scaffold pins ^5; both passed next build here. And the generated tsconfig.json has
"strict": false; set it to true for a new project, or you give up most of what TypeScript offers.
A real pitfall of this route: on npm 11.13, npm init -y writes "type": "commonjs" into package.json. Turbopack
then refuses ES-module syntax in the .tsx files (JSX compiles to an import) and every request returns HTTP 500.
⨯ ./app/layout.tsx
Error: Specified module format (CommonJs) is not matching the module format of the source code (EcmaScript Modules)
The CommonJs module format was specified in the package.json that is affecting this source file ...
GET / 500 in 988ms
The fix is to delete the "type" line (or set it to "module"); after removing it the same files returned HTTP 200.
create-next-app never writes the field.
Source: Installation: manual installation (the error output itself was observed in the scratch project)
For the Pages Router, the only difference is the folder: create pages/index.tsx instead of app/. There is no
required root layout. This minimal project also builds, and the route table prints Route (pages):
export default function Page() {
return <h1>Hello, Next.js!</h1>
}
Route (pages)
┌ ○ /
└ ○ /404
○ (Static) prerendered as static content
If you forget the root layout in an App Router project, next dev creates one for you when it starts.
The optional public folder at the project root serves static files from the base URL, so public/profile.png is
/profile.png. An optional src folder holds app (or pages) when you want to separate code from configuration.
The Commands: dev, build and start
The next binary is the whole toolchain. Three commands make the lifecycle; the others are helpers.
| Command | Purpose |
|---|---|
|
Development server with Hot Module Replacement, error reporting and on-demand compilation. Output goes to
|
|
Optimised production build; prints the route table. Since 16 it no longer runs the linter, and no longer prints bundle sizes. |
|
Starts the production server. The application must have been compiled with |
|
Prints operating system, Node.js, package manager and package versions for bug reports ( |
|
Generates the route types ( |
|
Upgrades Next.js (and its docs) to the latest version; available from 16.1. |
|
Enables or disables the anonymous telemetry ( |
|
Turbopack bundle analyzer, labelled experimental. |
Source: next CLI
Running next without a command is an alias for next dev. That has a nasty consequence for a removed command: in
Next.js 16.3.8 next lint does not exist any more, and the CLI treats lint as the project directory of next dev:
$ npx next lint
Invalid project directory provided, no such directory: /path/to/bookshelf/lint
Use the lint script (eslint) instead. A project that still has next lint in its scripts can be migrated with
npx @next/codemod@canary next-lint-to-eslint-cli ..
Source: Installation: set up linting and Upgrading to version 16
next dev
$ npm run dev
> bookshelf@0.1.0 dev
> next dev
▲ Next.js 16.3.8 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.1.20:3000
✓ Ready in 98ms
✓ Running next.config.ts took 13ms
GET / 200 in 731ms (next.js: 656ms, application-code: 74ms)
GET / 200 in 11ms (next.js: 1.5ms, application-code: 10ms)
✓ Compiled in 9ms
The first request compiles the route on demand (Turbopack bundles lazily, only what the server is asked for), later
ones are served in milliseconds, and each saved edit prints Compiled in …. The request lines split the time into
framework (next.js) and your code (application-code).
| Option | Effect |
|---|---|
|
The port (default 3000). |
|
The hostname to bind (default 0.0.0.0, so it is reachable from other devices on the network). |
|
Turbopack is the default; |
|
Attaches the Node.js debugger to the process that runs your code (16.1). |
|
HTTPS with a self-signed certificate generated for localhost, for webhooks and authentication callbacks. Use
|
|
A project directory other than the current one. |
Source: next dev options
next build
The build prints the route table. The legend is the quickest way to learn how Next.js classified each route. This is the table for the Bookshelf as built in the next section (previous model):
▲ Next.js 16.3.8 (Turbopack)
Creating an optimized production build ...
✓ Compiled successfully in 2.3s
Running TypeScript ...
Finished TypeScript in 869ms ...
Collecting page data using 7 workers ...
✓ Generating static pages using 7 workers (8/8) in 245ms
Finalizing page optimization ...
Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /books
├ /books/[id]
│ ├ ● /books/1
│ ├ ● /books/2
│ └ ● /books/3
└ ƒ /search
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses generateStaticParams)
ƒ (Dynamic) server-rendered on demand
Source: next build options
Useful next build options: --debug (also prints rewrites, redirects and headers), --debug-prerender (readable
stack traces for prerender errors, never deploy such a build), --debug-build-paths="app/page.tsx" (build one route
only), --webpack, --profile (React profiling in production) and --no-mangling.
next start
npm run build
PORT=3000 npm start # or: npx next start -p 3000
next start serves what next build produced, so a change to the source has no effect until you rebuild. Behind a
load balancer, pass --keepAliveTimeout <ms> larger than the balancer’s idle timeout. In 16.2 and later
next start --inspect attaches the Node.js debugger to the production server too.
Source: next start options
Turbopack and webpack
Since Next.js 16 Turbopack is the default bundler for both next dev and next build. Its file-system cache is on by
default (.next/dev/cache/turbopack for dev, .next/cache/turbopack for builds). A project with a custom webpack
configuration fails next build until you pick a bundler explicitly. To keep webpack, change the scripts:
{
"scripts": {
"dev": "next dev --webpack",
"build": "next build --webpack",
"start": "next start"
}
}
A next build --webpack of the Bookshelf was run and succeeded (the banner reads Next.js 16.3.8 (webpack)).
The life cycle in one picture
writes .next/dev"] B --> C{"Happy with the app?"} C -->|"no: edit and save"| B C -->|yes| D["npm run lint
eslint"] D --> E["npm run build
next build, writes .next"] E --> F["npm start
next start"] F --> G["Browser"] B --> G
The First Page and Layout, Step by Step
The starter page is replaced by pages that belong to the Bookshelf. Everything below was written into the scaffolded
project and then type-checked (tsc --noEmit), linted (eslint), built (next build) and served (next start).
Routing is explained in depth later in this section; here the goal is only to see the file conventions at work.
Next.js routes by file system: folders inside app define URL segments, and special files inside them define the
UI of that segment. A page.tsx makes a segment publicly reachable; a layout.tsx wraps its page and every nested
segment, keeps its state and does not re-render on navigation between those children.
The root layout
import type { Metadata } from 'next'
import Link from 'next/link'
import './globals.css'
export const metadata: Metadata = {
title: { default: 'Bookshelf', template: '%s | Bookshelf' },
description: 'A small catalogue of books, authors and reviews.',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<header>
<nav>
<Link href="/">Home</Link> <Link href="/books">Books</Link>
</nav>
</header>
<main>{children}</main>
</body>
</html>
)
}
Source: layout.js: root layout
The rules of the root layout, from the official reference:
-
It is required, lives at the top of
app, and must define<html>and<body>. -
Do not write
<head>tags such as<title>and<meta>by hand; exportmetadata(orgenerateMetadata) and Next.js streams and de-duplicates the head elements. Thetitle.templateabove turns a page titleBooksintoBooks | Bookshelf. -
There can be several root layouts: any layout with no
layout.jsabove it is a root layout, for example with the route groupsapp/(shop)/layout.jsandapp/(marketing)/layout.js. Navigating between different root layouts is a full page load, not a client-side navigation. -
The root layout may sit under a dynamic segment such as
app/[lang]/layout.js(internationalisation); that segment then becomes a root parameter. -
A layout cannot read the request object. It is cached on the client between navigations and does not re-render, so use
cookies()andheaders()in Server Components and Functions instead, not in the layout’s props.
A home page, a catalogue and a typed dynamic page
export type Book = { id: string; title: string; author: string }
export const books: Book[] = [
{ id: '1', title: 'Dune', author: 'Frank Herbert' },
{ id: '2', title: 'Solaris', author: 'Stanislaw Lem' },
{ id: '3', title: 'Neuromancer', author: 'William Gibson' },
]
export function getBook(id: string): Book | undefined {
return books.find((book) => book.id === id)
}
import Link from 'next/link'
export default function Page() {
return (
<>
<h1>Bookshelf</h1>
<p>
Browse the <Link href="/books">catalogue</Link>.
</p>
</>
)
}
import type { Metadata } from 'next'
import Link from 'next/link'
import { books } from '@/lib/books'
export const metadata: Metadata = { title: 'Books' }
export default function BooksPage() {
return (
<>
<h1>Books</h1>
<ul>
{books.map((book) => (
<li key={book.id}>
<Link href={`/books/${book.id}`}>{book.title}</Link> by {book.author}
</li>
))}
</ul>
</>
)
}
import { notFound } from 'next/navigation'
import { books, getBook } from '@/lib/books'
export function generateStaticParams() {
return books.map((book) => ({ id: book.id }))
}
export default async function BookPage(props: PageProps<'/books/[id]'>) {
const { id } = await props.params
const book = getBook(id)
if (!book) notFound()
return (
<article>
<h1>{book.title}</h1>
<p>by {book.author}</p>
</article>
)
}
Source: Layouts and pages, page.js and Link
Points worth noticing in these four files:
-
Linkis the primary navigation component. It extends<a>with prefetching and client-side transitions, so clickingBooksswaps the page without a full reload while the layout stays mounted. -
The module path alias
@/lib/bookscomes from thepathsentry written bycreate-next-app;getBookandbooksare imported from the project root, not through../../../. -
PageProps<'/books/[id]'>is a globally available helper type. It is generated into.next/types(and.next/dev/types) bynext dev,next buildornext typegen, so no import is needed, and it typesparamsfrom the route string. Static routes resolveparamsto an empty object. -
paramsis aPromiseand must be awaited. Next.js 15 made the request APIs asynchronous (with a temporary synchronous compatibility layer), and Next.js 16 removed the synchronous access. A page that readsparams.iddirectly is a leftover of the older model. -
generateStaticParamsreturns the segment values to prerender at build time, which is why the route table shows/books/1,/books/2and/books/3as●. A value outside the list is rendered on demand;notFound()renders thenot-foundUI and the response status is 404 (verified:/books/99returns 404 under the previous model). -
A page that is not a Client Component is a Server Component by default: it runs on the server (at build time here, because everything it needs is known), its code never reaches the browser, and the browser receives HTML plus the RSC payload used for later navigations.
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:3000/books/2
200
$ curl -si localhost:3000/books/2 | grep -i -E '^(x-nextjs|cache-control)'
x-nextjs-cache: HIT
x-nextjs-prerender: 1
x-nextjs-stale-time: 300
Cache-Control: s-maxage=31536000
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:3000/books/99
404
The prerendered page is served from the build output with a long shared-cache lifetime; this is where static generation, the subject of its own page later in the section, begins.
Source: generateStaticParams and page.js: params
A nested layout
A layout inside a folder wraps only that segment and below. The root layout wraps the books layout, which wraps both
/books and /books/[id].
export default function BooksLayout({
children,
}: {
children: React.ReactNode
}) {
return <section aria-label="Catalogue">{children}</section>
}
Search parameters make a page dynamic
A Server Component page reads the query string through the searchParams prop, which is also a Promise. Reading it
needs the incoming request, so the route stops being prerendered and is rendered on every request (the ƒ in the route
table).
import { books } from '@/lib/books'
export default async function SearchPage(props: PageProps<'/search'>) {
const { q } = await props.searchParams
const term = (Array.isArray(q) ? q[0] : q)?.toLowerCase() ?? ''
const hits = books.filter((book) => book.title.toLowerCase().includes(term))
return (
<>
<h1>Search</h1>
<ul>
{hits.map((book) => (
<li key={book.id}>{book.title}</li>
))}
</ul>
</>
)
}
$ curl -s 'localhost:3000/search?q=sol' | grep -o '<li>[^<]*</li>'
<li>Solaris</li>
$ curl -si 'localhost:3000/search?q=sol' | grep -i cache-control
Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
Use the searchParams prop when the parameters decide which data to load (filtering, pagination); use the client hook
useSearchParams when they only affect the client, for a list that was already loaded.
The same page under Cache Components
With cacheComponents: true the model is reversed: the build prerenders as much as it can into a static shell, and
anything that needs the request must be inside a <Suspense> boundary, or the build fails. The page above, built
unchanged with the option on, stops the build:
Error: Route "/search": Next.js encountered uncached or runtime data during prerendering.
`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>`
prevents the route from being prerendered, blocking the page load and leading to a slower user experience.
Ways to fix this:
- [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
- [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` ...
- [block] Set `export const instant = false` to allow a blocking route
Learn more: https://nextjs.org/docs/messages/blocking-prerender-dynamic
The error page blocking-prerender-dynamic documents the fix: stream the dynamic part behind a fallback. Under Cache
Components the route becomes partially prerendered (◐): the heading and the fallback are in the shell, the results
stream in.
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
import { Suspense } from 'react'
import { books } from '@/lib/books'
async function Results({
query,
}: {
query: PageProps<'/search'>['searchParams']
}) {
const { q } = await query
const term = (Array.isArray(q) ? q[0] : q)?.toLowerCase() ?? ''
const hits = books.filter((book) => book.title.toLowerCase().includes(term))
return (
<ul>
{hits.map((book) => (
<li key={book.id}>{book.title}</li>
))}
</ul>
)
}
export default function SearchPage(props: PageProps<'/search'>) {
return (
<>
<h1>Search</h1>
<Suspense fallback={<p>Searching...</p>}>
<Results query={props.searchParams} />
</Suspense>
</>
)
}
Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /books
├ /books/[id]
│ ├ ◐ /books/[id]
│ ├ ○ /books/1
│ ├ ○ /books/2
│ └ ○ /books/3
└ ◐ /search
○ (Static) prerendered as static content
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
Source: cacheComponents, blocking-prerender-dynamic and Caching
The extra ◐ /books/[id] row is the shell for book ids that were not listed in generateStaticParams: 16.3 can serve
an app shell for unknown params and upgrade it to a full page in the background. cacheComponents and
partialPrefetching are still opt-in in 16.3; the 16.3 announcement says their behaviour will become the default in a
future major version, which is why this section teaches both models and labels each example.
A first Client Component
Server Components cannot hold state or handle events. Mark the smallest interactive piece with 'use client' and
import it into the server page: the directive turns that file and everything it imports into client code that is
hydrated in the browser, while the page around it remains server-rendered.
'use client'
import { useState } from 'react'
export function LikeButton() {
const [likes, setLikes] = useState(0)
return <button onClick={() => setLikes(likes + 1)}>Like ({likes})</button>
}
import Link from 'next/link'
import { LikeButton } from '@/components/like-button'
export default function Page() {
return (
<>
<h1>Bookshelf</h1>
<p>
Browse the <Link href="/books">catalogue</Link>.
</p>
<LikeButton />
</>
)
}
Source: Server and Client Components and React: Server Components
| File | Runs | What reaches the browser |
|---|---|---|
|
Server, at build time (previous model: static and |
HTML, CSS links, and the RSC payload for client-side navigations. None of this component code is in the JavaScript bundle. |
|
Server, on every request (previous model) |
HTML streamed for each request; no search code in the bundle. |
|
Server for the first HTML, then client for hydration and every click |
Its JavaScript, |
|
Wherever it is imported from |
Nothing, while it is only imported by Server Components. If a Client Component imports it, the data ships in the bundle. |
Fast Refresh
Fast Refresh is a React feature integrated into Next.js that applies your edits in the browser within about a second while keeping temporary client-side state. It has been on by default since Next.js 9.4 and needs no configuration. Its behaviour depends on what the edited file exports:
-
A file that exports only React components is re-evaluated alone and its components re-render; styles, rendering logic, handlers and effects are all editable.
-
A file that also exports non-component values (a constant, a helper) is re-run together with the files that import it.
-
A file imported by something outside the React tree makes Fast Refresh fall back to a full reload.
Error resilience is part of the design. A syntax error shows an overlay; fixing it and saving removes the overlay with no reload and with no loss of state. A runtime error inside a component shows a contextual overlay; state is kept if the error did not occur while rendering, and React remounts the application with the new code if it did. Error boundaries retry on the next edit, so one around a section can stop a render error from resetting the whole page.
The behaviour was checked in a browser against the LikeButton above on a running next dev: the button was clicked
twice (Like (2)), the label in the source was changed to Favourite, and after the save the button read
Favourite (2) — the new code was live and the state survived.
- return <button onClick={() => setLikes(likes + 1)}>Like ({likes})</button>
+ return <button onClick={() => setLikes(likes + 1)}>Favourite ({likes})</button>
only what changed, with Turbopack"] B --> C{"What does the file export?"} C -->|"only React components"| D["Fast Refresh
re-render that module, keep state"] C -->|"components and other values"| E["Re-run the file
and the files that import it"] C -->|"imported outside the React tree"| F["Full reload"] B --> G{"Error?"} G -->|"syntax or build error"| H["Overlay in the browser"] G -->|"runtime error while rendering"| I["Overlay, then remount
after the next fix"] G -->|"no"| J["Browser shows the update in about a second"] H -->|"fix and save"| B I -->|"fix and save"| B D --> J E --> J
Source: Fast Refresh
When state is reset on every edit, one of these limitations applies, and each has a fix:
| Cause | What to do |
|---|---|
The file is a class component (only function components and Hooks keep state) |
Convert to a function component. |
The file exports something that is not a component next to the component |
Move the constant or helper to its own file. |
The file exports the result of a higher-order component such as |
Use a function component. |
An anonymous default export, such as |
Name the component. The |
An import whose case does not match the file name ( |
Fix the case; both fast and full refresh fail on it. |
Two tools help. Add the comment // @refresh reset anywhere in a file to make Fast Refresh remount that file’s
components on every edit, which is handy for an animation that only runs on mount. And remember that hooks with
dependency arrays (useEffect, useMemo, useCallback) always re-run during Fast Refresh, ignoring their
dependencies; code that tolerates an occasional extra effect run is better code anyway, and
React Strict Mode enforces it.
Source: Fast Refresh: limitations
The Dev Overlay and the Next.js DevTools
In next dev a small indicator in a corner of the page shows the state of the route you are looking at, and errors
open an overlay over the page. None of it exists in a production build.
| Feature | What it does | Since |
|---|---|---|
On-screen indicator |
Shows context about the current route, such as whether it is static or dynamic. |
15.0 (static indicator), 15.2 ( |
Error overlay |
Shows build, runtime and type errors with a code frame. A Node.js icon under the version label copies the URL of the server inspector to the clipboard, for debugging server errors in browser DevTools. |
Always; |
Hydration diff |
When server and client HTML disagree, the overlay shows a diff in which each differing line is labelled as coming from the client or from the server, so the diverging text is visible. |
16.2 |
Instant Insights |
A DevTools panel that surfaces the navigations you meet that are not instant, and offers a prompt that teaches an agent how to apply the fix. It is part of the Instant Navigations work. |
16.3 |
Navigation Inspector |
A DevTools panel with a Pause on navigations toggle that freezes a page load or a client navigation at its loading shell, to see what a user would see before dynamic data streams in. Prefetching is off in development, so this is the way to preview it. Available with Cache Components. |
16.3 |
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
devIndicators: {
position: 'bottom-right', // 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'
},
}
export default nextConfig
The options appIsrStatus, buildActivity and buildActivityPosition were deprecated in 15.2 and removed in 16.0; the
indicator itself remains. The indicator is the quickest check that a route you expect to be static really is: if it is
marked dynamic, the route uses a request-time API (cookies(), headers(), searchParams) or an uncached data
request. Confirm with next build --debug, where static routes print ○ and dynamic ones ƒ.
Source: devIndicators: troubleshooting
Debugging
Any debugger that attaches to Node.js works. The official guide gives VS Code launch configurations, browser DevTools and WebStorm. The two commands you need most are:
npm run dev -- --inspect # Node.js debugger for the server code (16.1+)
NODE_OPTIONS=--inspect-brk npm run dev # break on the first line
Open chrome://inspect and attach to the Next.js process for server code. For client code open the browser DevTools
Sources tab; the debugging guide says client sources appear under webpack://_N_E/./. A debugger; statement in
client code pauses the browser when it is reached. The React Developer Tools browser extension complements this with
the component tree, props and state, and its Suspense panel lists the <Suspense> boundaries of the page.
Source: Debugging and React Developer Tools
The DevTools MCP server
Next.js 16 added a Model Context Protocol endpoint, /_next/mcp, to the development server. The next-devtools-mcp
package lets a coding agent discover the running server and call tools such as get_errors, get_logs,
get_page_metadata, get_project_metadata and get_routes (added in 16.1), plus get_server_action_by_id,
get_compilation_issues and compile_route. Register it in .mcp.json at the project root:
{
"mcpServers": {
"next-devtools": {
"command": "npx",
"args": ["-y", "next-devtools-mcp@latest"]
}
}
}
Source: Next.js DevTools MCP server and Next.js 16
The server only exists while next dev runs, so restart the dev server if the agent cannot connect. Install the
package only in projects where you want agents to see the running application.
Cross-origin dev access
Next.js blocks cross-origin requests to dev-only assets and endpoints. localhost, its subdomains and the hostname
the server was started with are allowed; opening the app through another hostname (a tunnel, a custom domain, a
different machine) needs an allowedDevOrigins entry containing only the hostname, without scheme or port. Without it
the browser’s requests to those dev-only assets and endpoints are blocked.
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
allowedDevOrigins: ['*.tunnel.example.com'],
}
export default nextConfig
Source: allowedDevOrigins
A Tour of the App Router Getting-Started Sequence
The official documentation orders its App Router "Getting Started" chapter as a path. The table keeps that order and says what each step teaches and where it appears in this section. Pages of this section that are not written yet are named in plain text.
| Step | Official page | What it teaches and where this section goes deeper |
|---|---|---|
1 |
Create a project, requirements, manual install, TypeScript, linting, module path aliases, |
|
2 |
Folder and file conventions, colocation, private folders, route groups. The Project Structure page of this section. |
|
3 |
Pages, layouts, nested routes, dynamic segments, search params, |
|
4 |
Prefetching, prerendering, streaming and client-side transitions. Routing pages; see also React Routing for the client-only baseline. |
|
5 |
Which code runs where, |
|
6 |
Fetching in Server Components, streaming with |
|
7 |
Server Functions and Server Actions, forms. |
|
8 |
The caching model, including Cache Components. |
|
9 |
Time-based and on-demand revalidation. |
|
10 |
Expected errors versus uncaught exceptions, |
|
11 |
Tailwind CSS, CSS Modules, global CSS; see also React Styling and UI Libraries. |
|
12 |
The |
|
13 |
|
|
14 |
The Metadata API, file-based metadata, generated Open Graph images. |
|
15 |
HTTP endpoints built on the Web Request and Response APIs, as in the |
|
16 |
|
|
17 |
Node.js server, Docker, static export, adapters; see also React Build and Deployment. |
|
18 |
|
Source: App Router: Getting Started and the documentation index
Read the chapter in order the first time: each page assumes the previous one, and the dependencies between them (layouts before navigation, Server and Client Components before data fetching, fetching before caching) are the same as in the Bookshelf that this section builds.
Editor, Linting, Aliases and Upgrades
Editor setup
The App Router names files by convention, so an editor fills with tabs called page.tsx. The official page suggests
labelling each tab with its enclosing folders using VS Code 1.88 or later (or Cursor) custom editor labels; JetBrains
IDEs already show the folder.
{
"workbench.editor.customLabels.patterns": {
"**/app/**/page.tsx": "${dirname(1)}/${dirname} - page.tsx",
"**/app/**/layout.tsx": "${dirname(1)}/${dirname} - layout.tsx",
"**/app/**/route.ts": "${dirname(1)}/${dirname} - route.ts"
}
}
Source: Installation: set up your editor
To use the Next.js TypeScript plugin in VS Code, run "TypeScript: Select TypeScript Version" and choose "Use Workspace Version".
Linting
Use ESLint (the default) or Biome through plain package.json scripts. In Next.js 16 next build does not run the
linter and next lint is gone, so linting must be a step of your own in CI.
{ "scripts": { "lint": "eslint", "lint:fix": "eslint --fix" } }
{ "scripts": { "lint": "biome check", "format": "biome format --write" } }
Source: Installation: set up linting
Absolute imports and aliases
create-next-app writes the @/* alias for you. In a manual project, add paths (and optionally baseUrl) to
tsconfig.json or jsconfig.json; Next.js reads both. With baseUrl set to src/, each paths target is relative
to it.
{
"compilerOptions": {
"baseUrl": "src/",
"paths": {
"@/styles/*": ["styles/*"],
"@/components/*": ["components/*"]
}
}
}
Upgrading
npx next upgrade # 16.1 and later
npx @next/codemod@canary upgrade latest # before 16.1
npm i next@latest react@latest react-dom@latest eslint-config-next@latest # manually
Upgrading also updates the documentation under node_modules/next/dist/docs/, which is what the agent files point at.
Versions before 16.1.0 cannot run next upgrade.
Source: Upgrading
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
Every request returns 500 with "Specified module format (CommonJs) is not matching" |
|
Remove the field, or set it to |
|
The command was removed in 16; |
Use the |
|
Turbopack is the default and the version 16 upgrade guide says a custom webpack config fails the build |
Remove the customisation, or run |
|
Another process owns the port |
|
The page loads through a tunnel or custom host but dev features do not work |
The origin is not allowed in development |
Add the hostname to |
|
Since 15 they are Promises; 16 removed synchronous access |
|
The build fails with "uncached or runtime data during prerendering" |
|
Move the read into a child under |
A missing book returns HTTP 200 instead of 404 |
|
Call it before streaming begins, or add a |
The first |
The scaffold’s layout downloads Geist from Google Fonts |
Allow the network, or self-host the font with |
A change does not show after |
|
Run |
|
The |
Keep |
Where the Books Are Outdated
The two books consulted for this section — Real-World Next.js (Michele Riva, Packt, 2022) and Practical Next.js for E-Commerce (Alex Libby, Apress, 2023) — target the Pages Router on Next.js 12 and 13.2, with React 17 and 18. Their first chapters are the setup and project-layout chapters, which is where the differences from today are largest. They are good for the concepts and wrong for the commands.
| Topic | In the books | Today (16.3.8) |
|---|---|---|
Router in the template |
|
|
|
A short prompt list (TypeScript, ESLint, |
A recommended-defaults choice, then optional prompts for linter (ESLint, Biome or none), React Compiler, Tailwind,
|
Bundler |
webpack, custom |
SWC for transforms and Turbopack as the default for dev and build (16); a custom |
Linting |
|
|
|
|
|
Minimum Node.js |
Node 14 or 16 (both end-of-life) |
20.9.0 or later. |
Request APIs and |
Synchronous |
|
Dev experience |
Fast Refresh and the red error overlay |
Fast Refresh, an overlay with |
|
A command that wrote an |
The command was removed in 14; use |
Starting from the framework |
Comparisons with Gatsby, Razzle and Nuxt |
Out of scope on this page; see the framework comparison on the Introduction & Architecture page of this section. |
Where the books use a library or technique that is mentioned only briefly on this page — next/head, getStaticProps,
getServerSideProps and a custom _app — the replacement is the Metadata API and Server Components, introduced in the
first-page section above and covered on later pages.
What Changed Recently
The changes that affect getting started, by the version that introduced them.
| Version | Change |
|---|---|
13.4 |
The App Router became stable. |
15.0 (2024-10-21) |
React 19 support; asynchronous request APIs; |
15.2 (2025-02-26) |
The improved dev indicator and its |
16.0 (2025-10-21) |
Turbopack the default for |
16.1 (2025-12-18) |
|
16.2 (2026-03-18) |
Hydration diff in the overlay; |
16.3 (2026-08-03) |
|
Source: Next.js 15, Next.js 16, 16.1, 16.2, 16.3 and the support policy
The support policy at the checked date: 16.x is Active LTS, 15.x is Maintenance LTS (security and critical fixes, with version 15.5.27 the latest backport), and 14.x and earlier are unsupported. A project started today should be on 16.
Where to Go Next
Read the Introduction & Architecture page of this section for what a React framework adds and how the App Router and
Pages Router relate, and the Project Structure page for the complete file-convention table and the final Bookshelf
layout. The Configuration, TypeScript and Environment Variables page covers next.config.ts, the TypeScript plugin
and typed routes, .env* load order and NEXT_PUBLIC_ inlining. Those pages are written alongside this one and
are named here in plain text so that no link points at a page that does not exist yet.
Beyond this section: React Getting Started for the library under Next.js, Server Rendering, SSG and React Server Components for the rendering ideas, NestJS vs Next.js for how a Next.js front end relates to a NestJS back end, and Django Getting Started for the equivalent first steps in a Python stack.