Next.js
Printed from:
Complete Next.js Cheatsheet
Next.js is React's most popular full-stack framework — file-system routing, React Server Components, Server Actions, streaming, ISR, and edge/Node runtimes. This sheet targets Next.js 15 (App Router) while noting Pages Router differences where useful.
Table of Contents
- Quick Start
- Project Structure
- App Router Routing
- Layouts, Templates, Pages
- Loading, Error & 404
- Server vs Client Components
- Data Fetching
- Caching & Revalidation
- Server Actions
- Route Handlers (API)
- Middleware
- Streaming & Suspense
- Metadata & SEO
- Images, Fonts, Scripts
- Styling
- Forms & Mutations
- Authentication Patterns
- Configuration (
next.config.js) - Environment Variables
- Pages Router (Legacy)
- Deployment
- Troubleshooting
- Quick Reference
Quick Start
12345678910111213# Create a new app
npx create-next-app@latest my-app
pnpm create next-app my-app
bun create next-app my-app
# Prompts: TypeScript? ESLint? Tailwind? `src/` dir? App Router? Turbopack? Import alias?
cd my-app
pnpm dev # http://localhost:3000 (Turbopack by default in 15)
pnpm build && pnpm start
pnpm lint
pnpm tsc --noEmit
package.json scripts:
123456789{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}
Project Structure
123456789101112131415161718192021my-app/ ├── app/ # App Router (recommended) │ ├── layout.tsx # root layout (required) │ ├── page.tsx # / │ ├── globals.css │ ├── (marketing)/ # route group (no URL segment) │ │ └── page.tsx │ ├── blog/ │ │ ├── page.tsx # /blog │ │ └── [slug]/ │ │ └── page.tsx # /blog/:slug │ └── api/ │ └── hello/route.ts # /api/hello ├── public/ # static assets, served from / ├── components/ ├── lib/ ├── middleware.ts # runs on every request ├── next.config.ts ├── tsconfig.json └── package.json
src/ layout also supported — Next finds src/app or app/ automatically.
App Router Routing
| File | Role |
|---|---|
page.tsx | A route (publicly accessible). |
layout.tsx | Shared shell; persists across child pages. |
template.tsx | Like layout but re-mounts on navigation. |
loading.tsx | Suspense fallback for the segment. |
error.tsx | Error boundary (client component). |
not-found.tsx | Renders when notFound() is called. |
default.tsx | Parallel route fallback. |
route.ts | API handler (no UI). |
[param] | Dynamic segment. |
[...slug] | Catch-all. |
[[...slug]] | Optional catch-all. |
(group) | Route group, no URL impact. |
_private | Convention: not routed. |
@named | Named slot for parallel routes. |
(.), (..), (...) | Intercepted routes. |
123456// app/blog/[slug]/page.tsx
export default async function Page({
params, searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [k: string]: string | string[] | undefined }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}
Next 15:
paramsandsearchParamsare async — you mustawaitthem.
Static params for generateStaticParams
123456export async function generateStaticParams() {
const posts = await db.posts.findMany();
return posts.map(p => ({ slug: p.slug }));
}
export const dynamicParams = false; // 404 unknown slugs
Navigation
123456789101112131415import Link from "next/link";
import { useRouter, usePathname, useSearchParams, redirect } from "next/navigation";
<Link href="/blog" prefetch>Blog</Link>
// Client component
const router = useRouter();
router.push("/dashboard");
router.replace("/login");
router.back();
router.refresh(); // re-fetch current route's data
// Server component
redirect("/login");
Layouts, Templates, Pages
1234567// app/layout.tsx — root layout (must render <html> and <body>)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
1234567// app/(dashboard)/layout.tsx — nested layout
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<section className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</section>
);
}
Parallel & Intercepted Routes
12345678app/ ├── @sidebar/page.tsx # named slot ├── @main/page.tsx ├── layout.tsx # receives { sidebar, main, children } └── photos/ ├── [id]/page.tsx # full page at /photos/123 └── (.)photos/[id]/page.tsx # modal when navigated from same level
Loading, Error & 404
123// app/dashboard/loading.tsx
export default function Loading() { return <Spinner />; }
12345678// app/dashboard/error.tsx
"use client";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<p>{error.message}</p>
<button onClick={reset}>Retry</button>
</div>
);
}
123456// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
const post = await db.post.find(slug);
if (!post) notFound();
123// app/not-found.tsx
export default function NotFound() { return <h1>404</h1>; }
global-error.tsx catches root-layout errors.
Server vs Client Components
123456789// Server Component (default in app/)
// - Runs only on the server
// - Can be async, can read DB / env / fs
// - Cannot use useState, useEffect, browser APIs, event handlers
async function Page() {
const posts = await db.posts.findMany();
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
12345678// Client Component
"use client";
import { useState } from "react";
export default function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}
Rules of thumb:
- Default to Server Components.
- Mark a file
"use client"only when you need state, effects, refs, browser APIs, or event handlers. - You can pass Server Components as
childrenof a Client Component — but not the other way around.
Common imports:
12345import { Suspense, cache } from "react";
import { unstable_cache } from "next/cache";
import { headers, cookies } from "next/headers"; // both async in Next 15
import { redirect, notFound } from "next/navigation";
Data Fetching
123456789// In Server Components, just await
const posts = await db.posts.findMany();
// Or fetch — Next augments fetch with caching/revalidation
const res = await fetch("https://api.example.com/data", {
next: { revalidate: 60, tags: ["data"] },
});
const data = await res.json();
Per-request control
12345// Disable caching for this fetch
fetch(url, { cache: "no-store" });
// or
fetch(url, { next: { revalidate: 0 } });
Route segment config
1234567// app/blog/page.tsx
export const dynamic = "auto"; // "force-dynamic" | "force-static" | "error"
export const revalidate = 60; // seconds, or false
export const fetchCache = "auto";
export const runtime = "nodejs"; // or "edge"
export const preferredRegion = ["iad1"];
Streaming with Suspense
12345678import { Suspense } from "react";
export default function Page() {
return (
<>
<Header />
<Suspense fallback={<Skeleton />}>
<SlowList />
</Suspense>
</>
);
}
Caching & Revalidation
Next 15 has four caches: Request Memoization, Data Cache, Full Route Cache, Router Cache.
Default behaviour (Next 15)
fetch()is not cached by default — opt in with{ cache: "force-cache" }ornext: { revalidate }.- GET Route Handlers are not cached by default — opt in with
export const dynamic = "force-static"or useunstable_cache. - Client-side router cache stays for the session.
Time-based revalidation
12345fetch(url, { next: { revalidate: 3600 } }); // every hour
// or at segment level
export const revalidate = 3600;
Tag-based revalidation
123456789import { revalidateTag, revalidatePath } from "next/cache";
fetch(url, { next: { tags: ["posts"] } });
// In a Server Action or Route Handler:
revalidateTag("posts");
revalidatePath("/blog");
revalidatePath("/blog/[slug]", "page");
Memoizing arbitrary work
12345678import { unstable_cache } from "next/cache";
export const getPosts = unstable_cache(
async () => db.posts.findMany(),
["all-posts"], // key parts
{ revalidate: 60, tags: ["posts"] }
);
Server Actions
Functions that run only on the server, callable from client or server components.
12345678910111213// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createPost(formData: FormData) {
const title = String(formData.get("title"));
await db.post.create({ data: { title } });
revalidatePath("/blog");
redirect("/blog");
}
123456789// app/blog/new/page.tsx — used with a plain <form>
import { createPost } from "@/app/actions";
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}
From a Client Component
1234567891011"use client";
import { useTransition } from "react";
import { createPost } from "@/app/actions";
export function NewPostForm() {
const [pending, start] = useTransition();
return (
<form action={(fd) => start(() => createPost(fd))}>
<input name="title" />
<button disabled={pending}>Save</button>
</form>
);
}
useActionState (React 19)
123456789101112"use client";
import { useActionState } from "react";
const initial = { error: null as string | null };
export function LoginForm({ action }: { action: (s: typeof initial, fd: FormData) => Promise<typeof initial> }) {
const [state, formAction, pending] = useActionState(action, initial);
return (
<form action={formAction}>
<input name="email" />
<button disabled={pending}>Sign in</button>
{state.error && <p>{state.error}</p>}
</form>
);
}
Route Handlers (API)
app/api/hello/route.ts — exports HTTP verbs as functions.
1234567891011121314151617181920import { NextResponse, type NextRequest } from "next/server";
export const runtime = "nodejs"; // or "edge"
export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
return NextResponse.json({ ok: true });
}
export async function POST(req: NextRequest) {
const body = await req.json();
return NextResponse.json({ received: body }, { status: 201 });
}
// Dynamic params (Next 15: async!)
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
return NextResponse.json({ id });
}
NextResponse helpers:
12345NextResponse.json(data, { status: 200 });
NextResponse.redirect(new URL("/login", req.url));
NextResponse.rewrite(new URL("/internal", req.url));
NextResponse.next();
Middleware
middleware.ts at the project root runs on every matched request.
1234567891011121314151617import { NextResponse, type NextRequest } from "next/server";
export function middleware(req: NextRequest) {
const session = req.cookies.get("session");
if (!session && req.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", req.url));
}
return NextResponse.next();
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico).*)",
"/dashboard/:path*",
],
};
Runs on the Edge runtime — limited Node APIs.
Streaming & Suspense
1234567891011121314// app/blog/page.tsx
import { Suspense } from "react";
async function Posts() {
const posts = await getPosts(); // slow
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
export default function Page() {
return (
<>
<h1>Blog</h1>
<Suspense fallback={<p>Loading...</p>}>
<Posts />
</Suspense>
</>
);
}
loading.tsx is auto-wrapped in <Suspense> around the segment.
<PPR> Partial Prerendering (experimental)
123// next.config.ts
export default { experimental: { ppr: "incremental" } } satisfies NextConfig;
123// page.tsx
export const experimental_ppr = true;
Metadata & SEO
12345678910// app/layout.tsx (or any page.tsx)
import type { Metadata } from "next";
export const metadata: Metadata = {
title: { default: "My App", template: "%s · My App" },
description: "...",
openGraph: { images: ["/og.png"] },
twitter: { card: "summary_large_image" },
};
Dynamic:
123456export async function generateMetadata({
params,
}: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return { title: post.title, description: post.excerpt };
}
File-based metadata:
12345678app/icon.png -> favicon app/apple-icon.png app/opengraph-image.tsx -> generated OG image app/twitter-image.tsx app/robots.ts -> robots.txt app/sitemap.ts -> sitemap.xml app/manifest.ts -> manifest.webmanifest
123456// app/sitemap.ts
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [{ url: "https://example.com", lastModified: new Date() }];
}
Images, Fonts, Scripts
1234import Image from "next/image";
<Image src="/hero.jpg" alt="" width={1200} height={600} priority />
<Image src={remoteUrl} alt="" fill sizes="100vw" />
next.config.ts:
123456789import type { NextConfig } from "next";
const config: NextConfig = {
images: {
remotePatterns: [{ protocol: "https", hostname: "images.example.com" }],
formats: ["image/avif", "image/webp"],
},
};
export default config;
1234import { Inter, Geist_Mono } from "next/font/google";
const inter = Inter({ subsets: ["latin"], display: "swap" });
<html lang="en" className={inter.className}>...</html>
123import Script from "next/script";
<Script src="https://www.googletagmanager.com/gtag/js?id=G-X" strategy="afterInteractive" />
Styling
123456789101112// Global CSS — only in app/layout.tsx (root) or layout.tsx
import "./globals.css";
// CSS Modules
import styles from "./button.module.css";
// Tailwind (default in create-next-app)
<div className="grid grid-cols-2 gap-4" />
// CSS-in-JS — requires StyledJSXRegistry pattern for SSR
// styled-components, emotion, vanilla-extract all supported
Forms & Mutations
1234567891011121314// Plain HTML form + Server Action
<form action={createPost}>
<input name="title" required />
<button>Create</button>
</form>
// Client validation + optimistic UI
"use client";
import { useFormStatus } from "react-dom";
function Submit() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving..." : "Save"}</button>;
}
Authentication Patterns
1234567891011// Reading cookies in a Server Component (Next 15: async)
import { cookies } from "next/headers";
const c = await cookies();
const token = c.get("session")?.value;
// Setting cookies in a Server Action or Route Handler
import { cookies } from "next/headers";
const c = await cookies();
c.set("session", token, { httpOnly: true, sameSite: "lax", secure: true });
c.delete("session");
Popular libraries:
- Auth.js (next-auth v5): providers, sessions, JWT/database, middleware-friendly.
- Clerk / Stack Auth / Lucia / WorkOS / Better-Auth for full hosted auth.
Configuration (next.config.js)
1234567891011121314151617181920212223242526272829303132// next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = {
reactStrictMode: true,
experimental: {
ppr: "incremental",
typedRoutes: true,
serverActions: { bodySizeLimit: "2mb" },
},
images: {
remotePatterns: [{ protocol: "https", hostname: "**.cloudfront.net" }],
},
async redirects() {
return [{ source: "/old", destination: "/new", permanent: true }];
},
async rewrites() {
return [{ source: "/api/proxy/:path*", destination: "https://api.example.com/:path*" }];
},
async headers() {
return [{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
],
}];
},
};
export default config;
Environment Variables
123456.env always loaded .env.local always loaded, gitignored .env.development .env.production .env.test
123DATABASE_URL=postgresql://...
NEXT_PUBLIC_SITE_URL=https://example.com # exposed to the browser
123process.env.DATABASE_URL // server only
process.env.NEXT_PUBLIC_SITE_URL // client + server
Typed env (recommended): use @t3-oss/env-nextjs or zod in a env.ts module.
Pages Router (Legacy)
1234567pages/ ├── _app.tsx ├── _document.tsx ├── index.tsx # / ├── blog/[slug].tsx # /blog/:slug └── api/hello.ts # /api/hello
12345678910111213// SSG
export async function getStaticProps() {
return { props: { posts: await db.posts.findMany() }, revalidate: 60 };
}
export async function getStaticPaths() {
return { paths: [], fallback: "blocking" };
}
// SSR
export async function getServerSideProps(ctx) {
return { props: { user: await getUser(ctx.req) } };
}
123456// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from "next";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({ ok: true });
}
App Router and Pages Router can coexist in the same project.
Deployment
12345678910111213141516171819# Vercel (zero-config)
vercel
vercel --prod
# Self-host (Node)
pnpm build
pnpm start
# Docker
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN corepack enable && pnpm install --frozen-lockfile && pnpm build
CMD ["pnpm", "start"]
# Static export (no server features)
# next.config.ts: output: "export"
pnpm build # outputs ./out
For Cloudflare / Netlify / AWS, use their official adapters (@opennextjs/cloudflare, etc.).
Troubleshooting
123456789101112131415161718192021222324252627# "Hydration mismatch"
# - avoid random/Date.now in components rendered on both server and client
# - wrap browser-only code in useEffect or behind a client component
# - guard with typeof window !== "undefined"
# "Module not found"
# check tsconfig "paths" / "baseUrl" matches imports
# Stale data
# - revalidatePath / revalidateTag after mutations
# - check "use client" boundary
# Edge runtime crash
# - some Node APIs (fs, child_process) aren't available
# - set export const runtime = "nodejs"
# Build slow / out of memory
NODE_OPTIONS=--max-old-space-size=4096 pnpm build
# Clear caches
rm -rf .next node_modules pnpm-lock.yaml
pnpm install
# Verbose
NEXT_PRIVATE_DEBUG_CACHE=1 pnpm build
DEBUG=next:* pnpm dev
Quick Reference
1234567891011121314151617181920212223242526# Project
npx create-next-app@latest
pnpm dev / build / start / lint
# Routing (app/)
page.tsx layout.tsx loading.tsx error.tsx not-found.tsx route.ts
[slug] [...slug] [[...slug]] (group) @slot (.)intercept
# Cache control (per fetch)
fetch(url, { cache: "no-store" })
fetch(url, { next: { revalidate: 60, tags: ["x"] } })
# Cache invalidation
revalidatePath("/blog");
revalidateTag("posts");
# Navigation
<Link href="/x" />
const r = useRouter(); r.push("/x");
redirect("/login"); notFound();
# Async APIs (Next 15)
const { slug } = await params;
const c = await cookies();
const h = await headers();
Tip: start with Server Components by default. Reach for
"use client"only when state, effects, or browser APIs demand it. Co-locate Server Actions with their forms — they replace half your/api/*endpoints.
Continue Learning
Discover more cheatsheets to boost your productivity