Next.js 16 Cheatsheet
Comprehensive reference for Next.js 16 features including App Router, Server Components, Cache Components, Server Actions, async params/searchParams, and best practices
Table of Contents
- Prerequisites
- Installation & Setup
- Project Structure
- App Router & Routing
- Server Components
- Client Components
- Data Fetching
- Cache Components
- Server Actions
- Route Handlers
- Middleware & Proxy
- Styling
- Configuration
- Deployment
- Best Practices
- Common Pitfalls
Prerequisites
System Requirements π
- Node.js: Version 20.9 or later (required)
- TypeScript: Version 5.1 or later (if using TypeScript)
- React: Version 19.2+ (bundled with Next.js 16)
- Browsers: Chrome, Edge, Firefox 111+, Safari 16.4+
# Check Node.js versionnode --version# β v20.9.0 or higher
# Check TypeScript version (if installed)tsc --version# β Version 5.1.0 or higherKey Changes in Next.js 16 β οΈ
- Turbopack: Default bundler (replaces Webpack)
- Cache Components: New
'use cache'directive replacesforce-static - Async Params:
paramsandsearchParamsare now Promises - Proxy:
middleware.tsreplaced withproxy.ts(Node.js runtime only) - AMP Support: Removed entirely
Installation & Setup
Create New Project π
# Using pnpm (recommended)pnpm create next-app@latest my-next-app
# Using npmnpx create-next-app@latest my-next-app
# Using yarnyarn create next-app my-next-app
# With TypeScript and App Routerpnpm create next-app@latest my-app --typescript --appUpgrade Existing Project π
# Upgrade Next.js and Reactpnpm add next@latest react@latest react-dom@latest
# Upgrade TypeScript (if using)pnpm add -D typescript@latest @types/node@latest @types/react@latest @types/react-dom@latestDevelopment Server π₯οΈ
# Start dev server (Turbopack is default)pnpm dev
# Use Webpack instead (if needed)pnpm dev --webpack
# Start on specific portpnpm dev --port 3001Project Structure
App Router Directory Structure π
my-next-app/βββ app/ # App Router directoryβ βββ layout.tsx # Root layout (required)β βββ page.tsx # Home page (/)β βββ loading.tsx # Loading UIβ βββ error.tsx # Error UIβ βββ not-found.tsx # 404 pageβ βββ global.css # Global stylesβ βββ about/β β βββ page.tsx # /about routeβ βββ blog/β β βββ layout.tsx # Blog layoutβ β βββ page.tsx # /blog routeβ β βββ [slug]/β β βββ page.tsx # Dynamic route /blog/:slugβ βββ api/β β βββ users/β β βββ route.ts # API route /api/usersβ βββ (auth)/ # Route group (doesn't affect URL)β βββ login/β β βββ page.tsx # /loginβ βββ register/β βββ page.tsx # /registerβββ public/ # Static assetsβββ components/ # Shared componentsβββ lib/ # Utility functionsβββ next.config.js # Next.js configurationβββ tsconfig.json # TypeScript configβββ package.jsonFile Conventions π
| File | Purpose | Required |
|---|---|---|
layout.tsx | Shared UI for segment | Root: Yes |
page.tsx | Unique UI for route | Yes |
loading.tsx | Loading UI | No |
error.tsx | Error UI | No |
not-found.tsx | 404 UI | No |
route.ts | API endpoint | No |
template.tsx | Re-rendered layout | No |
App Router & Routing
Basic Routes π£οΈ
// app/page.tsx - Home route (/)export default function Home() { return <h1>Home Page</h1>;}
// app/about/page.tsx - /about routeexport default function About() { return <h1>About Page</h1>;}
// app/contact/page.tsx - /contact routeexport default function Contact() { return <h1>Contact Page</h1>;}Dynamic Routes π
export default async function BlogPost({ params,}: { params: Promise<{ slug: string }>;}) { const { slug } = await params; return <h1>Blog Post: {slug}</h1>;}
// app/shop/[...slug]/page.tsx - Catch-all routeexport default async function Shop({ params,}: { params: Promise<{ slug: string[] }>;}) { const { slug } = await params; return <h1>Shop: {slug?.join("/")}</h1>;}
// app/docs/[[...slug]]/page.tsx - Optional catch-allexport default async function Docs({ params,}: { params: Promise<{ slug?: string[] }>;}) { const { slug } = await params; return <h1>Docs: {slug?.join("/") || "Home"}</h1>;}Route Groups π¦
// app/(marketing)/about/page.tsx β /about// app/(marketing)/contact/page.tsx β /contact// app/(shop)/products/page.tsx β /products
// Route groups don't affect URL structure// Useful for organizing routes and applying layoutsParallel Routes β‘
// app/@analytics/page.tsx - Slot for analytics// app/@team/page.tsx - Slot for team// app/dashboard/layout.tsx - Uses @analytics and @team slots
export default function DashboardLayout({ children, analytics, team,}: { children: React.ReactNode; analytics: React.ReactNode; team: React.ReactNode;}) { return ( <> {children} {analytics} {team} </> );}Intercepting Routes π
// app/(.)login/page.tsx - Intercepts /login at same level// app/(..)login/page.tsx - Intercepts /login one level up// app/(...)login/page.tsx - Intercepts /login from root
// Useful for modals and overlaysNavigation with Link π
import Link from 'next/link'
// Basic link<Link href="/about">About</Link>
// With query parameters<Link href="/blog/1">Blog</Link>
// Using object syntax<Link href={{ pathname: '/blog', query: { page: '1', sort: 'date' } }}> Blog</Link>
// Prefetching (default: true)<Link href="/about" prefetch={false}>About</Link>
// Replace instead of push<Link href="/login" replace>Login</Link>
// External link (no prefetch)<Link href="https://example.com" target="_blank" rel="noopener noreferrer"> External</Link>Programmatic Navigation π§
"use client";
import { useRouter } from "next/navigation";
export default function Component() { const router = useRouter();
const handleClick = () => { router.push("/dashboard"); // router.replace('/dashboard') // Replace current history entry // router.back() // Go back // router.forward() // Go forward // router.refresh() // Refresh current route };
return <button onClick={handleClick}>Navigate</button>;}Server Components
Basic Server Component π₯οΈ
// Server Components are default - no directive needed
export default async function ServerComponent() { // Can use async/await directly const data = await fetch("https://api.example.com/data"); const json = await data.json();
return <div>{json.title}</div>;}β Doβs
- Use Server Components by default
- Fetch data directly in Server Components
- Use async/await for data fetching
- Pass data as props to Client Components
- Use Server Components for static content
β Donβts
- Donβt use hooks (
useState,useEffect) in Server Components - Donβt use browser APIs (
window,document) - Donβt use event handlers (
onClick,onChange) - Donβt use
'use client'directive in Server Components
Accessing Request Data π₯
import { headers, cookies } from "next/server";
export default async function Page() { // Read headers const headersList = await headers(); const userAgent = headersList.get("user-agent");
// Read cookies const cookieStore = await cookies(); const theme = cookieStore.get("theme")?.value;
return <div>User Agent: {userAgent}</div>;}Async Params & SearchParams π
export default async function BlogPost({ params, searchParams,}: { params: Promise<{ slug: string }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;}) { // Must await params and searchParams const { slug } = await params; const { page } = await searchParams;
return ( <div> <h1>Post: {slug}</h1> <p>Page: {page}</p> </div> );}
// Using PageProps helper (TypeScript)import type { PageProps } from "next";
export default async function BlogPost(props: PageProps<"/blog/[slug]">) { const { slug } = await props.params; const query = await props.searchParams; return <h1>Blog Post: {slug}</h1>;}Client Components
Basic Client Component π»
"use client"; // Must be first line
import { useState, useEffect } from "react";
export default function ClientComponent() { const [count, setCount] = useState(0);
useEffect(() => { console.log("Component mounted"); }, []);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;}Using Params in Client Components π
"use client";
import { use } from "react";
export default function Page({ params, searchParams,}: { params: Promise<{ slug: string }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;}) { // Use React.use() to unwrap promises in Client Components const { slug } = use(params); const { query } = use(searchParams);
return ( <div> Slug: {slug}, Query: {query} </div> );}β Doβs
- Use
'use client'directive at top of file - Use hooks (
useState,useEffect, etc.) - Use browser APIs and event handlers
- Keep Client Components small and focused
- Pass data from Server Components as props
β Donβts
- Donβt fetch data directly in Client Components (use Server Components)
- Donβt use Server-only APIs (
headers,cookies) in Client Components - Donβt make entire app a Client Component unnecessarily
Data Fetching
Fetch API with Caching Strategies π‘
// Static data (cached at build time)export default async function Page() { const res = await fetch("https://api.example.com/data", { cache: "force-cache", // Default }); const data = await res.json(); return <div>{data.title}</div>;}
// Dynamic data (refetch on every request)export default async function Page() { const res = await fetch("https://api.example.com/data", { cache: "no-store", }); const data = await res.json(); return <div>{data.title}</div>;}
// Time-based revalidationexport default async function Page() { const res = await fetch("https://api.example.com/data", { next: { revalidate: 3600 }, // Revalidate every hour }); const data = await res.json(); return <div>{data.title}</div>;}
// Tag-based revalidationexport default async function Page() { const res = await fetch("https://api.example.com/data", { next: { tags: ["posts"] }, }); const data = await res.json(); return <div>{data.title}</div>;}Database Queries ποΈ
import { sql } from "@vercel/postgres";
export default async function Page() { const { rows } = await sql` SELECT * FROM posts WHERE published = true `; return ( <ul> {rows.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> );}Loading States β³
export default function Loading() { return <div>Loading blog posts...</div>;}
// Or use Suspenseimport { Suspense } from "react";
export default function Page() { return ( <Suspense fallback={<div>Loading...</div>}> <BlogPosts /> </Suspense> );}Error Handling β οΈ
"use client";
export default function Error({ error, reset,}: { error: Error & { digest?: string }; reset: () => void;}) { return ( <div> <h2>Something went wrong!</h2> <button onClick={() => reset()}>Try again</button> </div> );}Cache Components
Using 'use cache' Directive πΎ
// File-level caching"use cache";
export default async function Page() { const data = await fetch("https://api.example.com/data"); return <div>{/* ... */}</div>;}
// Component-level cachingexport async function Bookings({ type }: { type: string }) { "use cache"; const data = await fetch(`/api/bookings?type=${type}`); return <div>{/* ... */}</div>;}
// Function-level cachingexport async function getProducts() { "use cache"; const data = await db.query("SELECT * FROM products"); return data;}Cache Life Configuration β±οΈ
import { cacheLife } from "next/cache";
export default async function Page() { "use cache"; cacheLife("hours"); // Cache for hours // cacheLife('days') // Cache for days // cacheLife('max') // Maximum cache lifetime
const data = await fetch("https://api.example.com/data"); return <div>{/* ... */}</div>;}Cache Tags π·οΈ
import { cacheTag } from "next/cache";
export async function getPosts() { "use cache"; cacheTag("posts");
const posts = await fetchPosts(); return posts;}
// Revalidate by tagimport { revalidateTag } from "next/cache";
export async function updatePost(id: string) { await db.posts.update(id, data); revalidateTag("posts", "max"); // Second arg is cacheLife profile}Update Tag (Read-Your-Writes) βοΈ
"use server";
import { updateTag } from "next/cache";
export async function updateUserProfile(userId: string, profile: Profile) { await db.users.update(userId, profile); updateTag(`user-${userId}`); // Expires cache, fetches fresh data}β Doβs
- Use
'use cache'for static or infrequently changing data - Use
cacheTag()to mark cached data for revalidation - Use
updateTag()for read-your-writes semantics - Use
cacheLife()to control cache duration
β Donβts
- Donβt use runtime APIs (
cookies(),headers()) in cached components - Donβt cache user-specific data without proper tags
- Donβt forget to revalidate when data changes
Server Actions
Basic Server Action π―
"use server";
export async function createPost(formData: FormData) { const title = formData.get("title") as string; const content = formData.get("content") as string;
await db.posts.create({ title, content }); revalidatePath("/blog");}
// app/components/CreatePost.tsx("use client");
import { createPost } from "../actions";
export default function CreatePost() { return ( <form action={createPost}> <input name="title" type="text" /> <textarea name="content" /> <button type="submit">Create Post</button> </form> );}Inline Server Actions π
export default function Page() { async function createInvoice(formData: FormData) { "use server"; const rawFormData = { customerId: formData.get("customerId"), amount: formData.get("amount"), }; // Mutate data }
return <form action={createInvoice}>{/* form fields */}</form>;}Server Actions with Extra Arguments π§
"use server";
export async function updateUser(userId: string, formData: FormData) { const name = formData.get("name") as string; await db.users.update(userId, { name });}
// app/components/UserProfile.tsx("use client");
import { updateUser } from "../actions";
export function UserProfile({ userId }: { userId: string }) { const updateUserWithId = updateUser.bind(null, userId);
return ( <form action={updateUserWithId}> <input name="name" type="text" /> <button type="submit">Update</button> </form> );}Server Actions with useActionState π¬
"use client";
import { useActionState } from "react";import { createPost } from "./actions";
export default function CreatePostForm() { const [state, formAction, isPending] = useActionState(createPost, null);
return ( <form action={formAction}> {state?.error && <p>{state.error}</p>} <input name="title" disabled={isPending} /> <button type="submit" disabled={isPending}> {isPending ? "Creating..." : "Create"} </button> </form> );}Revalidating After Actions π
"use server";
import { revalidatePath, revalidateTag } from "next/cache";
export async function updatePost(id: string, data: PostData) { await db.posts.update(id, data);
// Revalidate specific path revalidatePath("/blog");
// Or revalidate by tag revalidateTag("posts", "max");}Route Handlers
Basic Route Handler π£οΈ
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) { const users = await db.users.findMany(); return NextResponse.json(users);}
export async function POST(request: NextRequest) { const body = await request.json(); const user = await db.users.create(body); return NextResponse.json(user, { status: 201 });}HTTP Methods π‘
export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> },) { const { id } = await params; const post = await db.posts.find(id); return NextResponse.json(post);}
export async function PUT( request: NextRequest, { params }: { params: Promise<{ id: string }> },) { const { id } = await params; const body = await request.json(); const post = await db.posts.update(id, body); return NextResponse.json(post);}
export async function DELETE( request: NextRequest, { params }: { params: Promise<{ id: string }> },) { const { id } = await params; await db.posts.delete(id); return NextResponse.json({ deleted: true });}Reading Request Data π₯
import { NextRequest } from "next/server";
export async function POST(request: NextRequest) { // Read JSON body const body = await request.json();
// Read form data const formData = await request.formData();
// Read headers const contentType = request.headers.get("content-type");
// Read search params const searchParams = request.nextUrl.searchParams; const page = searchParams.get("page");
return NextResponse.json({ success: true });}Route Handler with Cache Components πΎ
import { cacheTag } from "next/cache";
async function getPosts() { "use cache"; cacheTag("posts"); const posts = await fetchPosts(); return posts;}
export async function GET() { const posts = await getPosts(); return Response.json(posts);}Middleware & Proxy
Proxy (Next.js 16) π
// proxy.ts (replaces middleware.ts)import { NextResponse } from "next/server";import type { NextRequest } from "next/server";
export function proxy(request: NextRequest) { // Modify request const requestHeaders = new Headers(request.headers); requestHeaders.set("x-pathname", request.nextUrl.pathname);
// Redirect if (request.nextUrl.pathname === "/old") { return NextResponse.redirect(new URL("/new", request.url)); }
// Rewrite if (request.nextUrl.pathname.startsWith("/api")) { return NextResponse.rewrite(new URL("/api-proxy", request.url)); }
// Add headers to response const response = NextResponse.next({ request: { headers: requestHeaders, }, });
response.headers.set("x-custom-header", "value"); return response;}Matcher Configuration π―
export const config = { matcher: ["/api/:path*", "/((?!_next/static|_next/image|favicon.ico).*)"],};β Doβs
- Use
proxy.tsfor request interception (Next.js 16+) - Keep proxy logic simple and fast
- Use matcher to limit which routes run proxy
- Return
NextResponse.next()to continue
β Donβts
- Donβt use
middleware.ts(replaced byproxy.ts) - Donβt use Edge Runtime APIs in proxy (Node.js runtime only)
- Donβt block requests unnecessarily
Styling
CSS Modules π¨
// app/components/Button.module.css.button { padding: 0.5rem 1rem; background: blue; color: white;}
// app/components/Button.tsximport styles from './Button.module.css'
export default function Button() { return <button className={styles.button}>Click me</button>}Tailwind CSS π¨
# Install Tailwindpnpm add -D tailwindcss postcss autoprefixernpx tailwindcss init -p@tailwind base;@tailwind components;@tailwind utilities;
// app/components/Card.tsxexport default function Card() { return ( <div className="rounded-lg bg-white p-6 shadow-md"> <h2 className="text-xl font-bold">Card Title</h2> </div> )}Global Styles π
import "./globals.css";
export default function RootLayout({ children,}: { children: React.ReactNode;}) { return ( <html lang="en"> <body>{children}</body> </html> );}CSS-in-JS (Styled Components) π
"use client";
import styled from "styled-components";
const Button = styled.button` padding: 0.5rem 1rem; background: blue; color: white;`;
export default function Component() { return <Button>Click me</Button>;}Configuration
next.config.js βοΈ
/** @type {import('next').NextConfig} */const nextConfig = { // React Compiler (automatic memoization) reactCompiler: true,
// Image optimization images: { domains: ["example.com"], remotePatterns: [ { protocol: "https", hostname: "**.example.com", }, ], },
// Environment variables env: { CUSTOM_KEY: "value", },
// Redirects async redirects() { return [ { source: "/old", destination: "/new", permanent: true, }, ]; },
// Rewrites async rewrites() { return [ { source: "/api/proxy/:path*", destination: "https://api.example.com/:path*", }, ]; },
// Headers async headers() { return [ { source: "/api/:path*", headers: [{ key: "Access-Control-Allow-Origin", value: "*" }], }, ]; },};
module.exports = nextConfig;TypeScript Configuration π
{ "compilerOptions": { "target": "ES2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": ["./*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"]}Deployment
Vercel Deployment π
# Install Vercel CLIpnpm add -g vercel
# Deployvercel
# Production deploymentvercel --prodEnvironment Variables π
# .env.local (development)DATABASE_URL=postgresql://...API_KEY=secret-key
# .env.production (production)DATABASE_URL=postgresql://...API_KEY=production-key// Access environment variablesconst apiKey = process.env.API_KEY;const dbUrl = process.env.DATABASE_URL;Build & Start ποΈ
# Build for productionpnpm build
# Start production serverpnpm start
# Analyze bundlepnpm build --analyzeStatic Export π¦
const nextConfig = { output: "export", // Static export images: { unoptimized: true, // Required for static export },};
module.exports = nextConfig;Best Practices
β Doβs
- Use Server Components by default - Only add
'use client'when needed - Fetch data in Server Components - Pass data as props to Client Components
- Use
'use cache'for static data - Improve performance with Cache Components - Await params and searchParams - Theyβre Promises in Next.js 16
- Use Suspense for loading states - Better UX than loading.tsx alone
- Revalidate after mutations - Use
revalidatePathorrevalidateTag - Use TypeScript - Better type safety and developer experience
- Optimize images - Use
next/imagecomponent - Use Route Handlers for API endpoints - Not Server Actions
- Keep Client Components small - Minimize JavaScript sent to client
β Donβts
- Donβt fetch in Client Components - Use Server Components instead
- Donβt use runtime APIs in cached components -
cookies(),headers()wonβt work - Donβt forget to await params/searchParams - Theyβre Promises now
- Donβt use
middleware.ts- Useproxy.tsin Next.js 16 - Donβt make entire app a Client Component - Only mark what needs interactivity
- Donβt ignore error boundaries - Always handle errors gracefully
- Donβt skip loading states - Users need feedback during data fetching
- Donβt forget to revalidate - Cache invalidation is crucial
Common Pitfalls
β οΈ Async Params Not Awaited
// β Wrong - params is a Promiseexport default function Page({ params }: { params: { slug: string } }) { const { slug } = params; // Error! return <div>{slug}</div>;}
// β
Correct - await the Promiseexport default async function Page({ params,}: { params: Promise<{ slug: string }>;}) { const { slug } = await params; return <div>{slug}</div>;}β οΈ Using Runtime APIs in Cached Components
// β Wrong - cookies() doesn't work in cached components"use cache";
export default async function Page() { const cookies = await cookies(); // Error! return <div>...</div>;}
// β
Correct - Remove 'use cache' or move logic outexport default async function Page() { const cookies = await cookies(); // Works! return <div>...</div>;}β οΈ Fetching in Client Components
// β Wrong - Fetching in Client Component"use client";
export default function Page() { const [data, setData] = useState(null);
useEffect(() => { fetch("/api/data") .then((res) => res.json()) .then(setData); }, []);
return <div>{data?.title}</div>;}
// β
Correct - Fetch in Server Componentexport default async function Page() { const res = await fetch("/api/data"); const data = await res.json(); return <div>{data.title}</div>;}β οΈ Not Revalidating After Mutations
// β Wrong - Cache not invalidated"use server";
export async function updatePost(id: string, data: PostData) { await db.posts.update(id, data); // Cache still shows old data!}
// β
Correct - Revalidate cache("use server");
import { revalidatePath, revalidateTag } from "next/cache";
export async function updatePost(id: string, data: PostData) { await db.posts.update(id, data); revalidatePath("/blog"); revalidateTag("posts", "max");}β οΈ Using Middleware Instead of Proxy
// β Wrong - middleware.ts (deprecated in Next.js 16)export function middleware(request: NextRequest) { // ...}
// β
Correct - proxy.ts (Next.js 16)// proxy.tsexport function proxy(request: NextRequest) { // ...}