App Router file conventions, data fetching and caching rules, Server Actions, config and metadata, and self-host/Docker/Vercel deploy recipes for Next.js 15 and 16.
Create a project
The interactive installer asks about TypeScript, ESLint, Tailwind, the src/ folder, the App Router, Turbopack, and an import alias. Say yes to TypeScript and the App Router unless you have a reason not to. Requires Node 18.18+ (Next 15) or Node 20.9+ (Next 16).
Know the project layout
Only app/ (or src/app/), public/, and next.config.* are special. Everything else is yours.
The scripts you'll run all day
Your first page, layout, and client component
Layouts wrap pages and persist across navigation (they don't re-render when you move between child routes). Link prefetches routes in the viewport, which is why Next apps feel instant.
Server Components can
async/await data, read files, query a DB, use secrets, import heavy libs. Cannot use hooks, state, event handlers, or browser APIs.
Client Components can
useState/useEffect, onClick, window, third-party UI libs. Everything they import is bundled to the browser.
Composition rule
Server can render Client; Client can't import Server — but can receive Server Components as children/props. Push 'use client' to the leaves.
Keep secrets on the server
import 'server-only' at the top of a module makes it a build error to import it from client code.
Vocabulary you'll meet in the docs
Version landmarks
Routing is the filesystem. A folder is a URL segment; a handful of reserved filenames inside it decide what renders. Files that aren't on this list (components, tests, styles) can live anywhere in app/ without becoming routes.
| File / folder | URL / role | Example | Notes |
|---|---|---|---|
| page.tsx | The UI for a route. Makes the folder publicly reachable. | app/pricing/page.tsx → /pricing | Default export a component. Only page and route make URLs; a folder with neither is just organisation. |
| layout.tsx | Shared shell for a segment and everything under it. | app/dashboard/layout.tsx | Receives children. Persists across sibling navigation (state kept, no re-render). Root layout must render <html> and <body>. Layouts don't get searchParams. |
| template.tsx | Like layout, but re-mounts on every navigation. | app/template.tsx | Use for enter animations or per-page effects that must re-run. Rare. |
| loading.tsx | Instant loading UI (Suspense boundary) while the segment's data loads. | app/blog/loading.tsx | Return a skeleton. Wraps page automatically; streams the real page in when ready. |
| error.tsx | Error boundary for the segment. | 'use client'; export default function Error({ error, reset }) | Must be a Client Component. reset() re-renders the segment. Doesn't catch errors in the same-level layout — put error.tsx one level up, or use global-error.tsx for the root. |
| not-found.tsx | 404 UI; triggered by notFound() or an unmatched URL. | import { notFound } from 'next/navigation'; if (!post) notFound(); | Root-level one handles every unmatched route on the site. |
| route.ts | API endpoint (Route Handler). Web-standard Request/Response. | app/api/contact/route.ts → POST /api/contact | Export GET, POST, PUT, DELETE… Can't sit next to a page.tsx in the same folder. See the Data tab for a full example. |
| [slug] | Dynamic segment. | app/blog/[slug]/page.tsx → /blog/hello | Read via the params prop — a Promise since Next 15: const { slug } = await params; |
| [...parts] | Catch-all: one or more segments. | app/docs/[...parts]/page.tsx → /docs/a/b/c | parts is an array. Docs sites, CMS-driven trees. |
| [[...parts]] | Optional catch-all: zero or more segments. | app/shop/[[...parts]]/page.tsx → /shop and /shop/x/y | Same page handles the index and every depth. |
| (group) | Route group: organise without affecting the URL. | app/(marketing)/about/page.tsx → /about | Each group can have its own layout.tsx — e.g. (marketing) with a big header, (app) with a sidebar. Multiple root layouts are allowed this way. |
| _folder | Private folder: never routed, even with a page inside. | app/_components/Button.tsx | Colocate components with the routes that use them without any risk of exposing them. |
| @slot | Parallel route: render two pages in one layout at once. | app/dashboard/@stats/page.tsx + @feed/page.tsx | Layout receives them as props ({ children, stats, feed }). Each slot needs a default.tsx for hard navigations. Dashboards, modals. |
| (.)path / (..)path | Intercepting route: show a route "inside" the current one (modal), full page on refresh. | app/feed/(..)photo/[id]/page.tsx | The Instagram photo-modal pattern. (.) same level, (..) one up, (...) from root. Combine with a parallel @modal slot. |
| default.tsx | Fallback for a parallel slot when the URL doesn't match it. | app/dashboard/@feed/default.tsx | Return null to render nothing. |
| proxy.ts (16) / middleware.ts (≤15) | Runs before every matched request: redirects, rewrites, auth gates, headers, geo/A-B. | export function proxy(req: NextRequest) { … } export const config = { matcher: ['/admin/:path*'] }; | Project root (or src/), one file only. Keep it light — it runs on every request in the matcher. Next 16 renamed it to proxy.ts (export proxy) and runs it on Node by default; the codemod migrates it. |
| instrumentation.ts | Runs once at server start. | export async function register() { … } | Sentry/OpenTelemetry init, warm a DB pool. |
| sitemap.ts / robots.ts | Generate /sitemap.xml and /robots.txt from code. | export default async function sitemap(): Promise<MetadataRoute.Sitemap> | Pull slugs from the DB/CMS so the sitemap is never stale. Static files in public/ work too. |
| opengraph-image.tsx / icon.tsx | Generate OG/social images and favicons per route. | import { ImageResponse } from 'next/og'; | JSX → PNG at request/build time. Or drop static opengraph-image.png / favicon.ico in the folder. |
| public/ | Static assets served from the site root. | public/logo.svg → /logo.svg | Not processed, not hashed — bump filenames for cache-busting or set headers in config. |
| pages/ (Pages Router) | The legacy router. pages/about.tsx → /about, pages/api/*.ts for APIs. | getStaticProps / getServerSideProps / getStaticPaths | Still supported. Data fetching via exported functions instead of async components. Can coexist with app/ during a migration. |
| Navigation API | Usage | Notes |
|---|---|---|
| <Link> | import Link from 'next/link'; <Link href="/blog/hi" prefetch={false}> | Client-side navigation + viewport prefetch. Works in Server Components. replace, scroll={false} props. |
| useRouter() | 'use client'; const r = useRouter(); r.push('/x'); r.replace(); r.back(); r.refresh(); | From next/navigation (not next/router — that's Pages Router). refresh() re-fetches Server Components without losing client state. |
| usePathname() / useSearchParams() | const path = usePathname(); const q = useSearchParams().get('q'); | Client hooks. Active-link styling, reading filters. useSearchParams needs a Suspense boundary or the page opts out of static rendering. |
| useParams() | const { slug } = useParams<{ slug: string }>(); | Dynamic segment values in a Client Component (Server Components get them via the params prop). |
| redirect() / permanentRedirect() | import { redirect } from 'next/navigation'; if (!user) redirect('/login'); | Server side (components, actions, handlers). Throws — don't wrap in try/catch. 307 vs 308. |
| notFound() | if (!post) notFound(); | Renders the nearest not-found.tsx with a 404 status. |
| generateStaticParams() | export async function generateStaticParams() { return posts.map(p => ({ slug: p.slug })); } | Pre-build dynamic routes at build time (SSG). Pair with dynamicParams = false to 404 anything else. |
Read data in a Server Component
Write data with a Server Action
A form posts straight to a server function. Works before JS loads (progressive enhancement). revalidatePath/revalidateTag refresh the cached pages that showed the old data.
Expose an API with a Route Handler
For webhooks, mobile clients, or anything that isn't your own React UI. Server Actions are usually better for your own forms.
Read the request: cookies, headers, searchParams
All async since 15. Using any of them makes the route dynamic (rendered per request).
Stream slow parts with Suspense
Send the shell immediately; stream the slow component in when its data arrives. loading.tsx does this for a whole segment; <Suspense> does it per component.
| Control | Syntax | Effect |
|---|---|---|
| Static (default) | — nothing to add — | A route with no request-time data is rendered once at next build and served as HTML. Fastest possible. |
| fetch with revalidate (ISR) | fetch(url, { next: { revalidate: 60 } }) | Cache the response for 60s, then regenerate in the background on the next hit. Stale-while-revalidate for pages. |
| fetch cached forever | fetch(url, { cache: 'force-cache' }) | Cache until manually revalidated. The old Next 13/14 default; explicit now. |
| fetch never cached | fetch(url, { cache: 'no-store' }) | Already the default in 15+. Also makes the route dynamic. |
| Cache tags | fetch(url, { next: { tags: ['posts'] } }) → revalidateTag('posts') | Label cached data; purge every page that used it in one call (from a Server Action or a webhook route handler when the CMS publishes). |
| revalidatePath | revalidatePath('/blog'); revalidatePath('/blog/[slug]', 'page'); | Purge one route (or every instance of a dynamic route). Call after a mutation. |
| Segment: revalidate | export const revalidate = 3600; | Page/layout-level ISR interval for everything in it. 0 = dynamic. false = cache forever. |
| Segment: dynamic | export const dynamic = 'force-dynamic' | 'force-static' | 'error' | 'auto'; | Force per-request rendering (dashboards), force static (and error if something dynamic sneaks in), or let Next infer. |
| Segment: dynamicParams | export const dynamicParams = false; | With generateStaticParams: unknown slugs 404 instead of rendering on demand. |
| Segment: runtime | export const runtime = 'nodejs' | 'edge'; | Edge = smaller, faster cold start, but no Node APIs (fs, most DB drivers). Default Node. Rarely worth switching. |
| unstable_cache / "use cache" | 'use cache'; export async function getPosts() { … } // 16 with cacheComponents; cacheLife('hours') | Cache the result of non-fetch work (DB queries) with the same revalidate/tag semantics. In 15 the API is unstable_cache(fn, keys, { revalidate, tags }). |
| React cache() | import { cache } from 'react'; export const getUser = cache(async (id) => db.user.find(id)); | Dedupe the same call across components in one request (fetch is deduped automatically; DB calls aren't). |
| Client router cache | experimental.staleTimes: { dynamic: 30, static: 180 } // next.config | How long the browser reuses a visited segment on back/forward or re-navigation. 15 sets dynamic pages to 0 (always fresh). |
| Partial Prerendering (PPR) | experimental.ppr: 'incremental'; export const experimental_ppr = true; | Static shell served instantly, dynamic holes streamed in — one route, both modes. Experimental in 15; the direction everything is heading (Cache Components in 16). |
| Opt out of everything | import { connection } from 'next/server'; await connection(); | "Wait for a real request before rendering" — for Math.random()/Date.now() pages that must not be baked at build (replaces unstable_noStore). |
| Thing | How | Notes |
|---|---|---|
| next.config.ts | import type { NextConfig } from 'next'; const config: NextConfig = { … }; export default config; | TypeScript config supported since 15. Restart dev after editing. |
| Env: server secrets | .env.local → DATABASE_URL=… process.env.DATABASE_URL | Only readable in server code. .env.local is gitignored by the starter; .env, .env.production are committed defaults. |
| Env: browser-visible | NEXT_PUBLIC_SITE_URL=https://acme.com | The NEXT_PUBLIC_ prefix inlines it into the client bundle at build time. Never put a secret here. Changing it needs a rebuild. |
| Images | import Image from 'next/image'; <Image src="/hero.jpg" width={1200} height={600} alt="…" priority /> | Auto WebP/AVIF, responsive srcset, lazy-load, no layout shift. Remote hosts must be allow-listed: images.remotePatterns: [{ hostname: 'cdn.acme.com' }]. Use fill + a sized parent when dimensions are unknown. |
| Fonts | import { Inter } from 'next/font/google'; const inter = Inter({ subsets: ['latin'] }); <body className={inter.className}> | Downloads at build, self-hosts, zero layout shift, no request to Google at runtime. next/font/local for your own files. Expose as a CSS variable with variable: '--font-inter' for Tailwind. |
| Metadata (static) | export const metadata: Metadata = { title: { default: 'Acme', template: '%s | Acme' }, description, openGraph: { … } }; | In any layout/page. Merged top-down. Also metadataBase for absolute OG URLs, alternates.canonical, robots. |
| Metadata (dynamic) | export async function generateMetadata({ params }): Promise<Metadata> { const p = await getPost((await params).slug); return { title: p.title }; } | Per-post titles/OG. Fetches are deduped with the page's own. |
| Scripts | import Script from 'next/script'; <Script src="https://www.googletagmanager.com/gtag/js?id=G-XXXX" strategy="afterInteractive" /> | Strategies: beforeInteractive, afterInteractive (default), lazyOnload. Google Analytics/Tag Manager helpers exist in @next/third-parties. |
| CSS | import './globals.css' | import s from './Card.module.css' | Tailwind | Sass (npm i sass) | Global CSS only in the root layout. CSS Modules anywhere. Tailwind v4 is the starter's default when you say yes. |
| Redirects / rewrites / headers | async redirects() { return [{ source: '/old', destination: '/new', permanent: true }]; } | In next.config. rewrites proxy silently (/api/:path* → https://api.acme.com/:path*); headers for CSP/HSTS. Dynamic rules go in proxy.ts. |
| basePath / assetPrefix | basePath: '/docs' | assetPrefix: 'https://cdn.acme.com' | Serve the app under a sub-path; serve _next/static from a CDN. |
| Import alias | import Button from '@/components/Button' | From tsconfig.json paths: { "@/*": ["./src/*"] }. Set by the installer. |
| Typed routes | typedRoutes: true | <Link href="/blgo"> becomes a type error. Stable in 15.5+. |
| Lint & format | npx eslint . | npx prettier --write . | Next 16 removed next lint; run ESLint directly (eslint-config-next is still the shared config). Biome is a popular single-tool alternative. |
| Bundle analysis | npm i -D @next/bundle-analyzer → ANALYZE=true npm run build | Find the 400 KB date library you imported into a Client Component. |
| Turbopack | next dev (default 15+) | next build --turbopack (15) / default (16) | --webpack to opt out (16) | Rust bundler replacing webpack. Custom webpack() config in next.config won't apply under Turbopack — use turbopack options or opt out. |
Date.now(), window, localStorage, or a browser extension editing the DOM. Move browser-only values into useEffect, or render them after mount.
'use client'. Add the directive — to the smallest component that needs it, not the page.
params, searchParams, cookies(), headers() async. npx @next/codemod@latest next-async-request-api . fixes a whole codebase.
next/image refuses remote URLs you haven't allow-listed. Add the host to images.remotePatterns, or use unoptimized for a one-off.
Build and read the route table
The build output tells you what each route is: ○ static, ● SSG with params, ƒ dynamic. If a page you expected to be static shows ƒ, something in it reads request data.
Self-host on a Linux server (standalone build + PM2 + nginx)
output: 'standalone' produces a self-contained folder with only the node_modules it needs — a few MB instead of hundreds. Copy it, run it, reverse-proxy it.
Docker (the same standalone build, containerised)
Static export (plain HTML for any host)
Everything is prerendered to out/. You lose Server Actions, Route Handlers that aren't static, ISR, proxy.ts, and default image optimisation — fine for brochure sites and docs.
Vercel
Or connect the GitHub repo in the dashboard: every push to main deploys production, every PR gets a preview URL. Set env vars per environment in Project → Settings.
- Secrets only in server code. Grep the built client bundle for anything sensitive:
grep -r "sk_live" .next/staticshould find nothing.import 'server-only'in every DB/secret module. - Server Actions are public endpoints. Anyone can call them with any input. Authenticate and validate (zod) inside the action, not just in the form.
- Set
metadataBaseor OG images resolve to localhost. - Handle the process properly when self-hosting: PM2 or systemd,
NODE_ENV=production, a health-check route, and log rotation.next startis a single Node process — put nginx or a CDN in front for TLS, gzip/brotli, and static caching. - Cache headers for the self-hosted static folder:
/_next/static/*is content-hashed — serve it withCache-Control: public, max-age=31536000, immutable. Never cache the HTML that long. - ISR on self-host uses the local filesystem cache. Multiple instances (containers, PM2 cluster) need a shared
cacheHandler(Redis) or they'll disagree about what's fresh. - Upgrade with the codemod (
npx @next/codemod@latest upgrade), read the version's upgrade guide, run the build. Major versions move things (async APIs in 15,proxy.tsin 16).
Official docs & further reading
The primary sources at nextjs.org. Every claim on this page can be checked against one of these — and the framework moves fast, so when this page and the docs disagree, the docs win.
create-next-app, the system requirements, the project structure it generates, and how to set up TypeScript, ESLint, and the import alias.
nextjs.org/docs/app/getting-started/installation
Learn
Go from beginner to expert by learning the foundations of Next.js and building a fully functional demo website. Free, hands-on, chapter-by-chapter — the App Router course is the best first afternoon with the framework.
nextjs.org/learn
Showcase
The web framework for when it matters. Peerless performance, production-grade — real sites built on Next.js, useful for showing a client what "we'll build it in Next" actually looks like.
nextjs.org/showcase
Blog
Release notes and announcements — each major (the Next.js 15 Release Candidate, 15, 16) ships with a post explaining what changed and why. Read the current release's post before upgrading.
nextjs.org/blog
GitHub
Source, issues, discussions, and the examples/ folder — hundreds of official starter templates (auth, CMS, Stripe, i18n, Docker…).
github.com/vercel/next.js