Next.js Cheatsheets
KDP Book Manifest & MetadataClick to Expand & CopyClick to CollapseManifest
Use the copy buttons below to copy metadata verbatim into the Amazon KDP Publishing forms.
- Books > Computers & Technology > Programming > Next.js
- Books > Computers & Technology > Web Development
Next.js Cheatsheets
One-Page Quick References from Core Syntax to Advanced Patterns
Next.js Cheatsheets
First Edition: 2026
Copyright © 2026 by usefulcheatsheets.com. All rights reserved.
No part of this book may be reproduced in any form or by any electronic or mechanical means, including information storage and retrieval systems, without written permission from the publisher, except for the use of brief quotations in a book review.
Table of Contents
Build REST APIs with route handlers, middleware, CORS, and auth patterns.
Use use server and use client directives to control rendering and data access.
Create dynamic pages with brackets, access route params, and generate static paths with generateStaticParams.
Manage environment variables and configure Next.js for different environments.
Organize Next.js projects for maintainability and performance.
Load web fonts efficiently with next/font for performance and UX.
Set up a Next.js project and start building pages and API routes quickly.
Optimize images in Next.js using the Image component for better performance.
Use the Link component and useRouter for fast client-side navigation in Next.js.
Build and handle API endpoints directly inside your Next.js application.
Table of Contents
Master file-based routing with the App Router for modern Next.js apps.
Understand ISR, revalidatePath, revalidateTag, and caching strategies for optimal performance.
Understand the Next.js SWC compiler, build options, and performance optimizations.
Build Next.js components using server and client rendering strategies for performance.
Fetch data in Next.js using server components, static generation, ISR, and client fetching.
Connect to databases using ORMs like Prisma, handle migrations, and query data safely.
Deploy Next.js apps to production on Vercel, Docker, and other platforms.
Implement custom error pages, error boundaries, and global error handling strategies.
Implement i18n in Next.js with routing, translations, locale detection, and multi-language support.
Generate metadata, open graph tags, and improve SEO automatically.
Table of Contents
Use Next.js middleware for redirects, auth checks, headers, and request rewriting in the App Router.
A practical guide to server components, the use client directive, and rendering boundaries.
Test Next.js apps with Jest, React Testing Library, and E2E testing with Playwright or Cypress.
Integrate analytics, error tracking, and performance monitoring into your Next.js app.
Profile apps, optimize bundle size, and implement advanced caching and streaming.
Stream content progressively with Suspense to improve perceived performance and UX.
Welcome to Next.js
Next.js is a key topic in Technology development.
This reference book compiles comprehensive cheatsheets covering everything from fundamentals to advanced patterns.
Use this book as a daily reference or read it linearly to build your knowledge.
How to Use This Book
Each page is a visual cheatsheet with core concepts, practical steps, code snippets, and warnings.
Next.js API Routes Patterns
Build REST APIs with route handlers, middleware, CORS, and auth patterns.
TL;DR
- 01Create API routes in app/api folder for backend endpoints.
- 02Use request and response objects to handle HTTP methods.
- 03Set up CORS headers to allow safe cross-origin API requests.
Tips
- 01Keep API logic separate by using helper functions and middleware to avoid duplicating common concerns like auth and CORS across routes.
Warnings
- 01Never expose secrets or sensitive data in API responses — always validate input and use proper authentication before returning user data.
Next.js API Routes Patterns
(continued)Basic Route Handlers
- Create a GET endpoint in app/api folder.
// app/api/users/route.ts export async function GET(request: Request) { return Response.json({ users: [] }); } - Handle different HTTP methods in the same file.
export async function GET(request: Request) { return Response.json({ method: "GET" }); } export async function POST(request: Request) { const data = await request.json(); return Response.json({ created: data }); } - Route handlers receive a Request and return a Response.
- Use Response.json() for JSON responses.
- Each function name corresponds to an HTTP method.
Next.js API Routes Patterns
(continued)Params and Catch-All Routes
- Access dynamic route params by awaiting the params object in Next.js 15.
// app/api/users/[id]/route.ts export async function GET( request: Request, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; // params is a Promise in Next.js 15 return Response.json({ userId: id }); } - Use the same await pattern for PUT, DELETE, and PATCH handlers.
export async function PUT( request: Request, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; const data = await request.json(); return Response.json({ updated: id }); } - Return 404 when the resource doesn't exist.
const { id } = await params; const user = await getUser(id); if (!user) { return Response.json({ error: "Not found" }, { status: 404 }); } return Response.json(user); - Use catch-all
[...path]segments for deeply nested API paths.// app/api/docs/[...path]/route.ts export async function GET( _req: Request, { params }: { params: Promise<{ path: string[] }> } ) { const { path } = await params; return Response.json({ segments: path }); } - Use optional catch-all
[[...path]]when the base segment should also match.app/api/search/[[...filters]]/route.ts # matches /api/search AND /api/search/active/recent
Next.js API Routes Patterns
(continued)Request Body Parsing
- Parse JSON from the request body.
export async function POST(request: Request) { const data = await request.json(); console.log(data); // { name: "Alice" } return Response.json({ success: true }); } - Handle form data for multipart uploads.
export async function POST(request: Request) { const formData = await request.formData(); const file = formData.get("file"); return Response.json({ uploaded: true }); } - Parse query parameters from the URL.
export async function GET(request: Request) { const { searchParams } = new URL(request.url); const query = searchParams.get("q"); return Response.json({ query }); } - Validate required fields before processing and return 400 for bad input.
const { name, email } = await request.json(); if (!name || !email) { return Response.json({ error: "name and email are required" }, { status: 400 }); } - Read a plain text body with request.text() for webhooks or raw payloads.
export async function POST(request: Request) { const raw = await request.text(); // e.g. Stripe webhook payload const sig = request.headers.get("stripe-signature"); return Response.json({ received: true }); }
Next.js API Routes Patterns
(continued)CORS and Headers
- Set CORS headers for cross-origin requests.
export async function GET(request: Request) { const response = Response.json({ data: "hello" }); response.headers.set("Access-Control-Allow-Origin", "*"); response.headers.set("Access-Control-Allow-Methods", "GET, POST"); return response; } - Use middleware for consistent header handling.
// middleware.ts export function middleware(request: Request) { const response = Response.next(); response.headers.set("Access-Control-Allow-Origin", "*"); return response; } - Return OPTIONS response for preflight requests with a 204 status.
export async function OPTIONS() { return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE", "Access-Control-Allow-Headers": "Content-Type, Authorization" } }); } - Restrict CORS to specific origins instead of wildcard for production.
const origin = request.headers.get("origin"); const allowed = ["https://myapp.com", "https://staging.myapp.com"]; const allowOrigin = allowed.includes(origin ?? "") ? origin : ""; response.headers.set("Access-Control-Allow-Origin", allowOrigin ?? ""); - Add Cache-Control and custom response headers to control caching and metadata.
return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", "Cache-Control": "no-store", "X-Request-Id": crypto.randomUUID() } });
Next.js API Routes Patterns
(continued)Auth and Error Patterns
- Database queries in API routes.
import { prisma } from "@/lib/prisma"; export async function GET() { const users = await prisma.user.findMany(); return Response.json(users); } - Error handling with status codes.
export async function POST(request: Request) { try { const data = await request.json(); if (!data.name) { return Response.json( { error: "Name required" }, { status: 400 } ); } return Response.json({ success: true }); } catch (error) { return Response.json( { error: "Invalid request" }, { status: 500 } ); } } - Authentication in API routes.
export async function GET(request: Request) { const token = request.headers.get("authorization"); if (!token) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } return Response.json({ data: "protected" }); } - Extract API keys from headers and compare against an environment variable.
const apiKey = request.headers.get("x-api-key"); if (apiKey !== process.env.API_SECRET) { return Response.json({ error: "Forbidden" }, { status: 403 }); } - Log errors server-side before returning a generic 500 message to the client.
} catch (err) { console.error("[api/users] error:", err); return Response.json({ error: "Internal server error" }, { status: 500 }); }
Next.js API Routes Patterns
(FAQ)FAQ
Export named functions matching each HTTP method from your route.ts file, such as export async function GET(request: Request) and export async function POST(request: Request). Requests with unhandled methods automatically return a 405 response.
In the App Router, params are passed as the second argument to the handler. In Next.js 15, params is a Promise — type it as { params: Promise<{ id: string }> } and await it before accessing properties: const { id } = await params. The route file goes at app/api/users/[id]/route.ts.
In App Router route handlers, the body is a ReadableStream — you must call await request.json() or await request.text() to consume it. Also ensure the client sends a Content-Type: application/json header when posting JSON.
Export a dedicated OPTIONS handler that returns the required CORS headers with a 204 status: return new Response(null, { status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,POST', 'Access-Control-Allow-Headers': 'Content-Type' } }). You must also include these headers on your actual GET/POST responses.
Pages Router uses pages/api/ with Node.js-style req/res objects, while App Router uses app/api/ with the standard Web Request/Response API. App Router route handlers support edge runtime and streaming natively, making them the better choice for new Next.js 13+ projects.
Next.js Directives
Use use server and use client directives to control rendering and data access.
TL;DR
- 01"use client" marks components to render on the client.
- 02"use server" marks functions to run only on the server.
- 03Server components are the default in the App Router.
Tips
- 01Use server components for fetching data and accessing secrets — it's faster and more secure than client components.
Warnings
- 01"use client" at the top of a file makes the entire file and its imports client-side — keep client-heavy components in separate files.
Next.js Directives
(continued)Default Server Rendering
- Every component in the App Router is a Server Component by default — no directive needed.
// app/page.tsx — server component, no directive required export default async function Home() { const res = await fetch("https://api.example.com/data", { cache: "no-store" }); const data = await res.json(); return <div>{data.title}</div>; } - Server Components run only on the server — no JavaScript is shipped to the browser for them.
- They can use async/await, read environment variables, and query databases securely.
export default async function AdminPage() { const users = await db.user.findMany(); return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>; } - Server Components cannot use React hooks or event handlers — add a directive to opt into client rendering only when needed.
// ❌ Not allowed in a Server Component — move to a Client Component // const [count, setCount] = useState(0); // window.addEventListener(...); - Use React cache() to deduplicate repeated server-side data calls across components.
import { cache } from "react"; export const getUser = cache(async (id: string) => db.user.findUnique({ where: { id } })); // getUser(id) called in Header and Sidebar = only one DB query
Next.js Directives
(continued)Client Directive Scope
- "use client" must be the very first line of the file — before any imports.
"use client"; // ↑ must appear before any import statements import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; } - The directive marks the entire file AND all modules it imports as client-side — contamination spreads through imports.
"use client"; // Both files below become part of the client bundle: import { Chart } from "./Chart"; // ← client-side import { DataGrid } from "./DataGrid"; // ← client-side - Avoid importing heavy utilities into a "use client" file — they inflate the browser bundle.
"use client"; // ❌ Pulls entire lodash into the client bundle import _ from "lodash"; // ✅ Move heavy computation to a Server Component or Server Action - Split a large client component into smaller files to minimise the client boundary.
// Good: only the interactive button is a Client Component // app/post/page.tsx — Server Component (no directive) import { LikeButton } from "./LikeButton"; // only this file has "use client" export default async function PostPage() { const post = await getPost(); return <article>{post.body}<LikeButton id={post.id} /></article>; } - Third-party components that use hooks or browser APIs must be wrapped with "use client".
// components/MapWrapper.tsx "use client"; import { MapComponent } from "some-map-library"; // uses window internally export default function MapWrapper({ center }) { return <MapComponent center={center} />; }
Next.js Directives
(continued)Server Functions
- Mark functions to run only on the server with "use server".
"use server"; export async function submitForm(formData: FormData) { const email = formData.get("email"); // Save to database securely return { success: true }; } - Server functions can be called from client components.
"use client"; async function handleSubmit(formData: FormData) { const result = await submitForm(formData); } - Functions run securely on the server.
- Use server functions with HTML form actions for progressive enhancement.
// app/form.tsx (server component) import { saveContact } from "./actions"; export default function ContactForm() { return <form action={saveContact}><input name="email" /><button>Send</button></form>; } - Validate input inside server functions before writing to the database.
"use server"; export async function createPost(formData: FormData) { const title = formData.get("title") as string; if (!title || title.length < 3) return { error: "Title too short" }; await db.post.create({ data: { title } }); return { ok: true }; }
Next.js Directives
(continued)Directive Boundary Patterns
- A Client Component can receive a Server Component as a child via props — the child still runs on the server.
"use client"; // ClientShell.tsx — the children prop can be a Server Component export default function ClientShell({ children }) { return <div>{children}</div>; } // app/page.tsx (Server Component) — ServerSidebar stays on the server <ClientShell><ServerSidebar /></ClientShell> - A Client Component cannot directly import a Server Component — this would pull server code into the client bundle.
"use client"; // ❌ Cannot import a server component directly from a client file import { ServerSidebar } from "./ServerSidebar"; // runtime error - Functions cannot cross the server-client boundary as props — use Server Actions instead.
// ❌ Plain server function cannot be passed as a prop <ClientComp onClick={serverFunction} /> // ✅ Server Action CAN be passed as a prop "use server"; export async function deleteItem(id: string) { await db.item.delete({ where: { id } }); } // Then in a Server Component: <ClientComp action={deleteItem} /> - Only JSON-serialisable values can be passed as props across the server-client boundary.
// ✅ OK: strings, numbers, arrays, plain objects, Promises <ClientChart data={data} title="Sales" /> // ❌ Not OK: class instances, Maps, Sets, functions (unless Server Actions) <ClientComp handler={someClassInstance} /> - Verify the boundary by checking the Network tab — server component code should not appear in JS bundles.
# Open DevTools > Network > JS # Server-only modules (db, secrets) should NOT appear in client chunks
Next.js Directives
(continued)Directive Design Patterns
- Use server components by default for performance.
// Good: server component fetches data export default async function Posts() { const posts = await getPosts(); return posts.map(post => <Post key={post.id} post={post} />); } - Mark leaf components as "use client" for interactivity.
"use client"; // Only interactive components export function Favorite({ postId }) { const [liked, setLiked] = useState(false); return <button onClick={() => setLiked(!liked)}>{liked ? "Liked" : "Like"}</button>; } - Use server-only package to prevent accidental client imports.
import "server-only"; // This file throws an error if imported in a client bundle export async function getSecretData() { ... } - Avoid passing non-serializable values like functions as props.
// Wrong: functions can't cross the server-client boundary as props <ClientComp onClick={serverFunction} /> // ❌ // Use server actions instead <ClientComp action={serverAction} /> // ✅ - Test directive placement by checking the Network tab for unexpected JS.
# Open browser DevTools > Network > JS # Check that server-only components don't appear in client bundles
Next.js Directives
(FAQ)FAQ
Add 'use client' when your component uses browser APIs, React hooks like useState or useEffect, or event listeners. Components that only fetch data or render static content should stay as server components.
Yes — Server Components can import and render Client Components, but not the other way around. Pass Server Components as children or props to Client Components if you need to compose them.
Placing 'use server' at the top of a file marks all exported functions as Server Actions, while placing it inside an individual async function marks only that function. Use the inline form when you need just one Server Action inside a Client Component.
If the component accessing the secret has 'use client' or is imported by a Client Component, it runs on the client. Move secret access into a Server Component or Server Action where the code never leaves the server.
No — Server Components can call fetch or query a database directly without any directive, since they already run only on the server by default in the App Router. 'use server' is specifically for Server Actions (functions called from the client).
Next.js Dynamic Routes
Create dynamic pages with brackets, access route params, and generate static paths with generateStaticParams.
TL;DR
- 01Wrap folder names in brackets to create dynamic segments.
- 02Access params through the params prop passed to pages.
- 03Use generateStaticParams to pre-build dynamic pages at compile time.
Tips
- 01Use <code>generateStaticParams()</code> for high-traffic pages like blog posts to pre-build them at deploy time for instant page loads.
- 02Combine <code>generateStaticParams()</code> with a <code>revalidate</code> export so high-traffic dynamic pages stay pre-built while still picking up fresh content automatically.
Warnings
- 01If generateStaticParams doesn't include a route, it will be generated on first request which causes a slow cold start on serverless.
- 02The object keys returned by <code>generateStaticParams()</code> must exactly match the dynamic segment names in the folder, or Next.js throws a build error: "generateStaticParams returned invalid params."
Next.js Dynamic Routes
(continued)Basic Dynamic Routes
- One folder name decides whether a route is static or matches infinite URLs — wrap it in square brackets to make it dynamic.
app/ posts/ [id]/ page.tsx - This matches
/posts/1,/posts/2, or any value for theidsegment. - Access the dynamic parameter through the
paramsprop, which is now an asyncPromiseyou must await.export default async function Post({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; return <h1>Post {id}</h1>; } - Each dynamic segment creates a separate route that can load different data.
- URL parameters are always strings, so parse them as needed.
Next.js Dynamic Routes
(continued)Accessing Route Parameters
- Nest two dynamic segments and the
paramsobject resolves both at once, once you await it.// Route: app/users/[userId]/posts/[postId]/page.tsx export default async function PostPage({ params }: { params: Promise<{ userId: string; postId: string }> }) { const { userId, postId } = await params; return <div>User {userId}, Post {postId}</div>; } - Fetch data using the resolved params to load content specific to that route.
const { id } = await params; const post = await getPost(id); - Pass params to layout components if they need this information.
- Nested dynamic segments work just like single-level ones.
Next.js Dynamic Routes
(continued)Catch-All Routes
- Need one file to match
/docs/a,/docs/a/b, and/docs/a/b/calike? Catch-all segments do exactly that with[...slug].app/ docs/ [...slug]/ page.tsx - This matches
/docs/a,/docs/a/b,/docs/a/b/c, etc. - The
slugparam is always an array of path segments, available after awaitingparams.export default async function Docs({ params }: { params: Promise<{ slug: string[] }> }) { const { slug } = await params; const path = slug.join("/"); return <h1>{path}</h1>; } - Use catch-all routes for nested documentation or content sites.
- Combine with optional catch-all
[[...slug]]for breadcrumb navigation.
Next.js Dynamic Routes
(continued)Optional Catch-All Routes
- What if one page needs to handle both
/blogand/blog/2025/january/post? Double the brackets to[[...slug]]and the catch-all becomes optional.app/ blog/ [[...slug]]/ page.tsx - This matches
/blog,/blog/post-1,/blog/2025/january/post, etc. - The slug param is an array only if segments exist, otherwise undefined — await
paramsfirst to read it.const { slug } = await params; const segments = slug ?? []; const depth = segments.length; - Use optional catch-all when a single page handles multiple URL patterns.
- Great for flexible navigation menus and hierarchical content.
Next.js Dynamic Routes
(continued)Static Generation with Dynamic Routes
- Skip the per-request server round trip entirely — export
generateStaticParams()to pre-build specific dynamic pages at deploy time.export async function generateStaticParams() { const posts = await getPosts(); return posts.map((post) => ({ id: post.id.toString(), })); } export default async function Post({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; // This page is pre-built for every post at build time return <h1>Post {id}</h1>; } - Pre-built pages load instantly with no server delay.
- Pages not in generateStaticParams are generated on-demand the first time.
- Use
revalidatefor incremental static regeneration to update stale pages.export const revalidate = 3600; // Rebuild every hour - Great for blogs, product pages, and public documentation.
Next.js Dynamic Routes
(FAQ)FAQ
Create a folder with the segment name wrapped in square brackets, like app/blog/[slug]/page.tsx. The bracket syntax tells Next.js that this segment is dynamic and will match any value at that URL position.
Dynamic page components receive a params prop, which is an async Promise containing your segment names as keys. For a route like [slug], type it as async function Page({ params }: { params: Promise<{ slug: string }> }), then run const { slug } = await params before using it.
The [...slug] (catch-all) requires at least one segment and returns 404 for the base path, while [[...slug]] (optional catch-all) also matches the parent route with no segments. Use optional catch-all when you want one component to handle both /docs and /docs/getting-started.
Yes, you can nest multiple dynamic folders, such as app/shop/[category]/[productId]/page.tsx. Both category and productId will be available on the params object simultaneously.
By default, Next.js falls back to server-rendering the missing page on first request. You can change this behavior by exporting export const dynamicParams = false from the page, which will make unlisted routes return a 404 instead.
Next.js Environment Variables
Manage environment variables and configure Next.js for different environments.
TL;DR
- 01Use .env.local for environment variables in development.
- 02Variables prefixed NEXT_PUBLIC_ are exposed to browser.
- 03Server-only variables are only available on the server.
Tips
- 01Use .env.example to document which environment variables are needed — commit it to version control.
Warnings
- 01Never commit .env.local files — they contain secrets. Use .gitignore to exclude them.
Next.js Environment Variables
(continued)Environment Files
- Create .env.local for local development variables.
# .env.local DATABASE_URL=postgresql://user:password@localhost/db API_SECRET=my-secret-key - Create .env.production.local for production secrets — Next.js loads it only in production and it is gitignored by default.
# .env.production.local DATABASE_URL=postgresql://user:password@prod/db API_SECRET=production-secret - Variables are loaded at build time for static files.
- Use .env.development to set variables for npm run dev only.
# .env.development NEXT_PUBLIC_API_URL=http://localhost:4000 LOG_LEVEL=debug - Use .env for shared defaults across all environments.
# .env (committed — no secrets) NEXT_PUBLIC_APP_NAME=My App NEXT_PUBLIC_SUPPORT_EMAIL=support@example.com - Next.js uses first-definition-wins: files are checked highest to lowest priority.
# Priority (highest to lowest): # 1. .env.{NODE_ENV}.local (e.g. .env.development.local) # 2. .env.local (skipped in test environment) # 3. .env.{NODE_ENV} (e.g. .env.development) # 4. .env (shared defaults) # The FIRST file that defines a variable wins — later files cannot override it.
Next.js Environment Variables
(continued)Public Variables
- Prefix with NEXT_PUBLIC_ to expose to browser.
# .env.local NEXT_PUBLIC_API_URL=https://api.example.com NEXT_PUBLIC_APP_NAME=My App - Access in client and server code.
function Component() { const apiUrl = process.env.NEXT_PUBLIC_API_URL; return <p>API: {apiUrl}</p>; } - Use for non-sensitive data only.
- Public variables are inlined at build time, not at runtime.
// process.env.NEXT_PUBLIC_API_URL becomes a string literal in the bundle const url = process.env.NEXT_PUBLIC_API_URL; // "https://api.example.com" - Use for things like Stripe publishable keys or analytics IDs.
NEXT_PUBLIC_STRIPE_KEY=pk_live_abc123 NEXT_PUBLIC_GA_ID=G-XXXXXXX
Next.js Environment Variables
(continued)Server-Only Variables
- Variables without NEXT_PUBLIC_ are server-only.
# .env.local DATABASE_URL=postgresql://... API_KEY=secret-key - Access only in server-side code.
export async function GET() { const apiKey = process.env.API_KEY; // Safe: runs on server only } - Never available in browser or client components.
- Use server-only variables in server actions and route handlers.
"use server"; export async function deleteUser(id: string) { await db.user.delete({ where: { id } }); // process.env.DATABASE_URL is safe here } - Attempting to access server-only vars in client code returns undefined.
"use client"; // process.env.API_KEY is undefined — not leaked to browser
Next.js Environment Variables
(continued)Runtime Config and Validation
- Expose build-time variables to the client via the env key in next.config.js.
// next.config.js module.exports = { env: { APP_VERSION: process.env.npm_package_version, // inlined at build time FEATURE_FLAG: process.env.FEATURE_FLAG } }; - Use serverRuntimeConfig for values available only on the server at runtime.
// next.config.js module.exports = { serverRuntimeConfig: { mySecret: process.env.MY_SECRET // server-only, not in browser }, publicRuntimeConfig: { apiUrl: process.env.NEXT_PUBLIC_API_URL // shared with client } }; - Validate required environment variables at startup to catch missing config immediately.
// lib/env.ts — imported in app/layout.tsx const required = ["DATABASE_URL", "AUTH_SECRET"]; for (const key of required) { if (!process.env[key]) throw new Error(`Missing env var: ${key}`); } - Use the t3-env or envalid library for type-safe, schema-validated environment variables.
import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; export const env = createEnv({ server: { DATABASE_URL: z.string().url() }, client: { NEXT_PUBLIC_API_URL: z.string().url() }, runtimeEnv: process.env }); - Restart the dev server after changing .env files — Next.js does not hot-reload environment variables.
# After editing .env.local or adding new variables: npm run dev # restart required for new vars to take effect
Next.js Environment Variables
(continued)Secrets and Version Control
- Never commit .env.local or .env.production.local to version control.
# .gitignore .env.local .env.development.local .env.test.local .env.production.local - Commit .env.example as documentation of required variables — never put real values in it.
# .env.example (safe to commit) DATABASE_URL=postgresql://user:password@localhost/db NEXT_PUBLIC_API_URL=https://api.example.com AUTH_SECRET= - Never access process.env in a Client Component for secrets — server-only variables return undefined in the browser and do NOT get bundled.
"use client"; // ❌ DATABASE_URL is undefined in the browser — not leaked, just missing const url = process.env.DATABASE_URL; - Use NEXT_PUBLIC_ only for truly public values — these are inlined as string literals and visible to anyone who reads the JS bundle.
# Safe to expose NEXT_PUBLIC_STRIPE_KEY=pk_live_abc123 NEXT_PUBLIC_GA_ID=G-XXXXXXX # Never prefix with NEXT_PUBLIC_ STRIPE_SECRET=sk_live_secret DATABASE_URL=postgres://prod-secret - Set environment variables in your CI/CD platform rather than committing .env.production files.
# Vercel CLI vercel env add DATABASE_URL production # GitHub Actions secret # Settings > Secrets > Actions > New repository secret
Next.js Environment Variables
(FAQ)FAQ
Prefix the variable name with NEXT_PUBLIC_ (e.g., NEXT_PUBLIC_API_URL) and Next.js will inline its value at build time, making it available in both client and server code. Variables without this prefix are stripped from the client bundle entirely.
.env is for default values shared across environments and can be committed to version control, while .env.local overrides those values locally and should never be committed. Next.js loads both files, but .env.local always takes precedence.
Make sure the variable is defined in your .env.local or .env file and that you are not accidentally prefixing it with NEXT_PUBLIC_ while only expecting server-side access. Also confirm the dev server was restarted after adding or changing the variable, since Next.js does not hot-reload env files.
Yes — use the env key in next.config.js to explicitly expose specific variables to the client, or use serverRuntimeConfig for server-only values and publicRuntimeConfig for values shared with the client at runtime rather than build time.
Next.js loads environment files in priority order from highest to lowest: .env.{NODE_ENV}.local, then .env.local (skipped in test), then .env.{NODE_ENV}, then .env. The first file that defines a variable wins — lower-priority files cannot override it. Use .env.local for secrets that should never be committed, and .env for shared defaults.
Next.js File Structure
Organize Next.js projects for maintainability and performance.
TL;DR
- 01Use app router for file-based routing with folders.
- 02Group related files with parentheses to exclude from routing.
- 03Colocate components, hooks, and utilities near where they're used.
Tips
- 01Organize by feature or domain first, not by file type — this keeps related code together and makes refactoring easier.
Warnings
- 01Don't create too many deeply nested folder levels — keep structures 3–4 levels deep for clarity.
Next.js File Structure
(continued)App Router Basics
Create routes by adding a folder and a page.tsx file inside it.
app/ page.tsx # / route about/ page.tsx # /about route blog/ page.tsx # /blog route [slug]/ page.tsx # /blog/:slug routeMap URL path segments directly to folder names in the app directory.
app/users/[id]/posts/page.tsx → /users/:id/postsAdd a root layout.tsx to wrap every page with shared HTML structure.
// app/layout.tsx export default function RootLayout({ children }: { children: React.ReactNode }) { return <html lang="en"><body>{children}</body></html>; }Use page.tsx for visible UI and route.ts for API route handlers.
app/ api/ users/ route.ts # GET /api/users, POST /api/users users/ page.tsx # /users page rendered in the browserAdd a loading.tsx next to any page to show a Suspense fallback.
// app/blog/loading.tsx export default function Loading() { return <p>Loading posts...</p>; }Use dynamic [slug] segments to match variable URL parts — in Next.js 15, params is a Promise.
// app/blog/[slug]/page.tsx export default async function Post({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; return <h1>Post: {slug}</h1>; }
Next.js File Structure
(continued)Special Files
Use layout.tsx to share persistent UI across child routes without remounting.
// app/layout.tsx export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html> <body> <Header /> {children} <Footer /> </body> </html> ); }Use error.tsx as an error boundary — it must be a Client Component.
// app/blog/error.tsx "use client"; export default function Error({ error, reset }: { error: Error; reset: () => void }) { return ( <div> <h2>Something went wrong</h2> <button onClick={reset}>Try again</button> </div> ); }Use loading.tsx to create an automatic Suspense boundary for the segment.
// app/blog/loading.tsx export default function Loading() { return <p>Loading posts...</p>; }Use not-found.tsx to render a custom 404 for each route segment.
// app/blog/[slug]/not-found.tsx export default function NotFound() { return <h1>Post not found</h1>; }Use template.tsx when the layout must remount on each navigation.
// app/dashboard/template.tsx export default function Template({ children }: { children: React.ReactNode }) { // Re-runs useEffect and re-mounts on every route change return <div>{children}</div>; }
Next.js File Structure
(continued)Route Groups
Wrap folder names in parentheses to exclude them from the URL path.
app/ (marketing)/ page.tsx # / (still at root URL) about/page.tsx # /about contact/page.tsx # /contact (dashboard)/ layout.tsx # separate layout, no URL impact page.tsx # /dashboardGive each route group its own layout for auth vs app separation.
app/ (auth)/ layout.tsx # auth layout with centered card login/page.tsx # /login signup/page.tsx # /signup (app)/ layout.tsx # app layout with sidebar nav dashboard/page.tsxScope middleware-like access control by grouping protected routes.
app/ (protected)/ dashboard/page.tsx settings/page.tsx (public)/ page.tsx about/page.tsxDefine a layout per group without affecting the URL structure.
// app/(auth)/layout.tsx export default function AuthLayout({ children }: { children: React.ReactNode }) { return <main className="auth-bg">{children}</main>; }Combine route groups with parallel routes using the @ prefix.
app/ (dashboard)/ @analytics/page.tsx @overview/page.tsx layout.tsx
Next.js File Structure
(continued)Module and Export Patterns
Use TypeScript path aliases to avoid deep relative imports across the project.
// tsconfig.json { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }Create barrel files with index.ts to expose a clean public API for each feature.
// features/auth/index.ts export { LoginForm } from "./components/LoginForm"; export { useAuth } from "./hooks/useAuth"; export type { AuthUser } from "./types";Mark server-only modules with the server-only package to prevent client imports.
// lib/db.ts import "server-only"; export const db = new PrismaClient();Use a global types/ folder for shared TypeScript interfaces across features.
types/ api.ts # shared API response shapes user.ts # User interface used app-wide post.ts # Post interface used app-wideRe-export shared UI primitives from a single index to keep import paths short.
app/ ui/ Button.tsx Card.tsx Modal.tsx index.ts # export { Button, Card, Modal }
Next.js File Structure
(continued)Scaling Patterns
Use feature folders to group all files for one domain in one place.
app/ features/ auth/ page.tsx components/ hooks/ utils.ts posts/ page.tsx components/ hooks/Keep all Next.js special files inside the feature folder.
features/users/ page.tsx layout.tsx error.tsx loading.tsx components/ UserCard.tsx hooks/ useUser.tsCreate barrel files with index.ts to simplify imports from feature folders.
// features/auth/index.ts export { LoginForm } from "./components/LoginForm"; export { useAuth } from "./hooks/useAuth";Place server actions in a dedicated actions/ folder inside each feature.
features/posts/ actions/ createPost.ts deletePost.ts components/ page.tsxPut cross-feature utilities in a root lib/ folder accessible to all routes.
lib/ format.ts # shared date/number formatting fetcher.ts # shared fetch wrapper constants.ts # app-wide constants
Next.js File Structure
(FAQ)FAQ
Page-specific components should live next to the route they belong to, inside the same folder. Shared components used across multiple routes go in a top-level 'components' directory or a '_components' folder inside 'app'.
Folders wrapped in parentheses like '(marketing)' are excluded from the URL path but still organize your file system. Use them to group routes by section or layout without affecting the actual routes users see.
Use the 'app' directory — it's the current standard and enables React Server Components, nested layouts, and streaming. The 'pages' router is still supported but is considered legacy for new projects.
'layout.tsx' persists across navigations and doesn't remount, making it ideal for sidebars and headers. 'template.tsx' creates a fresh instance on every navigation, so use it when you need effects or animations to re-run between routes.
Yes — only files named as special files (page.tsx, layout.tsx, etc.) are treated as routes. You can safely place hooks, utilities, and components alongside your route files and they won't become endpoints.
Next.js Font Optimization
Load web fonts efficiently with next/font for performance and UX.
TL;DR
- 01Use next/font to load Google Fonts automatically.
- 02Font files are hosted locally for faster loading.
- 03Subset and weight fonts to reduce bundle size.
Tips
- 01Use next/font to load Google Fonts — it handles optimization automatically and improves Core Web Vitals.
Warnings
- 01Don't load too many font weights or families — each adds overhead. Stick to 2–3 fonts with a few weights each.
Next.js Font Optimization
(continued)Google Fonts
- Import and use Google Fonts with next/font.
import { Inter } from "next/font/google"; const inter = Inter({ subsets: ["latin"] }); export default function RootLayout({ children }) { return ( <html className={inter.className}> <body>{children}</body> </html> ); } - Fonts are downloaded at build time and hosted locally.
- No additional network requests for fonts during page loads.
- Automatic font fallback to prevent layout shifts.
Next.js Font Optimization
(continued)Multiple Fonts
- Use multiple Google Fonts in the same project.
import { Inter, Playfair_Display } from "next/font/google"; const inter = Inter({ subsets: ["latin"] }); const playfair = Playfair_Display({ weight: ["400", "700"], subsets: ["latin"] }); export default function Page() { return ( <div> <h1 className={playfair.className}>Title</h1> <p className={inter.className}>Body text</p> </div> ); } - Apply different fonts to different elements.
- Reduce bundle size by specifying only needed weights.
- Use CSS variables to apply fonts globally via Tailwind or CSS.
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); export default function RootLayout({ children }) { return <html className={inter.variable}>{children}</html>; } // In CSS: font-family: var(--font-inter); - Export fonts from a shared module to avoid duplicate declarations.
// lib/fonts.ts import { Inter, Roboto_Mono } from "next/font/google"; export const inter = Inter({ subsets: ["latin"] }); export const mono = Roboto_Mono({ subsets: ["latin"] });
Next.js Font Optimization
(continued)Font Weights and Subsets
- Specify weights to load only what you need.
const inter = Inter({ weight: ["400", "500", "700"], subsets: ["latin"] }); - Use subsets to reduce font file size.
const playfair = Playfair_Display({ subsets: ["latin"], // Only Latin characters display: "swap" // Show fallback while loading }); - Display property controls fallback behavior.
const font = Inter({ display: "swap", // Show fallback immediately // "auto" (default), "block", "fallback", "optional" }); - Use adjustFontFallback to reduce cumulative layout shift further.
const inter = Inter({ subsets: ["latin"], adjustFontFallback: true // adjusts metrics of fallback font }); - Load a variable font instead of multiple weights for best performance.
const inter = Inter({ subsets: ["latin"] // Inter is a variable font — no weight array needed });
Next.js Font Optimization
(continued)Custom Fonts
- Load custom fonts from local files.
import localFont from "next/font/local"; const customFont = localFont({ src: [ { path: "../fonts/custom.woff2", weight: "400", style: "normal" }, { path: "../fonts/custom-bold.woff2", weight: "700", style: "normal" } ] }); - Place font files in the public folder.
public/ fonts/ custom.woff2 custom-bold.woff2 - Use the custom font in components.
- Apply a custom font via className on a wrapper element.
export default function RootLayout({ children }) { return ( <html className={customFont.className}> <body>{children}</body> </html> ); } - Use a CSS variable with a local font to integrate with Tailwind.
const customFont = localFont({ src: "../fonts/custom.woff2", variable: "--font-custom" }); // tailwind.config: fontFamily: { brand: ["var(--font-custom)"] }
Next.js Font Optimization
(continued)Performance Best Practices
- Use font display swap to prevent layout shift.
const inter = Inter({ display: "swap", // Shows fallback font immediately subsets: ["latin"] }); - Load fonts early in the layout for best results.
// app/layout.tsx (root layout) import { Inter } from "next/font/google"; const inter = Inter(); export default function RootLayout({ children }) { return <html className={inter.className}>{children}</html>; } - Subset fonts to languages you support.
const inter = Inter({ subsets: ["latin", "latin-ext"] // Add extended Latin }); - Measure font impact on Core Web Vitals using web-vitals.
import { getCLS } from "web-vitals"; getCLS(console.log); // CLS score should be near 0 with font optimization - Use preload: false to skip preloading a font that is not above the fold.
const mono = Roboto_Mono({ subsets: ["latin"], preload: false // Only loaded when used, not preloaded });
Next.js Font Optimization
(FAQ)FAQ
Import the font from next/font/google and call it as a function with your desired options — Next.js automatically downloads the font files at build time and serves them from your own domain, eliminating third-party requests.
Yes, import and instantiate each font separately from next/font/google, then apply their .className or CSS variable to the relevant elements. Keep the total count to 2–3 families to avoid unnecessary payload growth.
A standard @import fetches fonts from Google's CDN at runtime, which adds a render-blocking network request and can hurt Core Web Vitals. next/font self-hosts the files and injects optimized <link> tags with zero layout shift.
Pass weight and subsets arrays when instantiating the font, e.g. weight: ['400', '700'] and subsets: ['latin'] — only those variants are bundled, keeping the font payload as small as possible.
Import localFont from next/font/local and point src at your font file (or an array of files for multiple weights/styles) inside the public or app directory — you get the same automatic optimization as with Google Fonts.
Next.js Getting Started
Set up a Next.js project and start building pages and API routes quickly.
TL;DR
- 01Create a new Next.js project using create-next-app command.
- 02Pages go in the app folder and routes are file-based.
- 03Run npm run dev to start the development server.
Tips
- 01Start with create-next-app for fastest setup — it includes all necessary configurations and best practices.
Warnings
- 01Remember that files in the app folder automatically become routes — organize carefully to avoid unexpected routes.
Next.js Getting Started
(continued)Installation
- Create a new Next.js project.
npx create-next-app@latest my-app cd my-app npm run dev - Answer prompts to configure TypeScript, ESLint, etc.
- Dev server runs at http://localhost:3000.
- Use --ts flag to skip prompts and create a TypeScript project.
npx create-next-app@latest my-app --ts --app --tailwind - Install dependencies and start the dev server in one step.
npm install && npm run dev
Next.js Getting Started
(continued)Project Structure
- Pages in app folder become routes automatically.
my-app/ app/ page.tsx # / route about/ page.tsx # /about route blog/ [slug]/ page.tsx # /blog/:slug route public/ # Static files package.json - Each page.tsx file is a route.
- Place static assets like images in the public folder.
public/ logo.png # /logo.png favicon.ico # /favicon.ico - The layout.tsx at the root wraps every page in the app.
// app/layout.tsx export default function RootLayout({ children }) { return <html lang="en"><body>{children}</body></html>; } - Use the app/api folder to create API route handlers.
// app/api/health/route.ts export async function GET() { return Response.json({ status: "ok" }); }
Next.js Getting Started
(continued)Creating a Page
- Create a simple page component.
// app/page.tsx export default function Home() { return ( <main> <h1>Welcome to Next.js</h1> <p>Get started by editing this page</p> </main> ); } - Pages are server components by default.
// app/about/page.tsx export default function About() { return <h1>About Us</h1>; } - Fetch data inside a server page using async/await.
export default async function Blog() { const posts = await getPosts(); return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>; } - Add metadata to set the page title and description.
export const metadata = { title: "Home", description: "Welcome to my Next.js site" }; - Create a client component for interactive pages.
"use client"; import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>Count: {count}</button>; }
Next.js Getting Started
(continued)Building and Deployment
- Build for production and start the server locally to verify the output.
npm run build npm start # Route Size First Load JS # / 3 kB 87 kB ← aim for < 130 kB - Check the build output table after every build — First Load JS above 130 kB signals bundle bloat.
npm run build 2>&1 | grep "First Load JS" # Flag any route above 130 kB for optimisation - Deploy to Vercel in seconds — it auto-detects Next.js and requires no config.
npm install -g vercel vercel # auto-detected, deployed immediately - Set environment variables before the first deployment — .env.local is not deployed.
vercel env add DATABASE_URL production vercel env add NEXT_PUBLIC_API_URL production - Set output:standalone in next.config.js when building a Docker image — reduces image size by excluding node_modules.
// next.config.js module.exports = { output: "standalone" };
Next.js Getting Started
(continued)Essential Add-ons
- Add Tailwind CSS for utility-first styling — available as a prompt option in create-next-app.
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p - Use the Link component for client-side navigation — never use plain <a> tags for internal routes.
import Link from "next/link"; export default function Nav() { return <nav><Link href="/about">About</Link></nav>; } - Use Prisma as your database ORM — integrates directly with Server Components and Server Actions.
npm install prisma @prisma/client npx prisma init npx prisma db push # sync schema to database - Add NextAuth.js or Clerk for authentication — both integrate with the App Router middleware.
npm install next-auth # or npm install @clerk/nextjs - Use next/image for optimised images — automatically serves WebP and prevents layout shift.
import Image from "next/image"; <Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority />
Next.js Getting Started
(FAQ)FAQ
Run npx create-next-app@latest my-app and follow the interactive prompts to configure TypeScript, ESLint, and Tailwind CSS. The scaffolded project includes the app router, proper folder structure, and all dependencies pre-installed.
The app directory uses the newer App Router with React Server Components by default, while pages uses the legacy Pages Router. New projects should use app — it supports co-located layouts, loading UI, and server actions that the Pages Router lacks.
Create nested folders inside app, and any folder containing a page.tsx file becomes a URL segment. For example, app/dashboard/settings/page.tsx maps to /dashboard/settings, and you can add a shared layout.tsx at any level to wrap child routes.
Run npm run build to generate an optimized production build in the .next folder, then npm start to serve it. For Vercel deployments, pushing to your connected repo triggers both steps automatically with zero configuration.
The App Router only treats a file named exactly page.tsx (or page.js) as a route — other filenames like index.tsx or home.tsx inside a folder are ignored as routes. Make sure the file is named page.tsx and lives inside the app directory, then restart the dev server.
Next.js Image Optimization
Optimize images in Next.js using the Image component for better performance.
TL;DR
- 01Use next/image instead of img tags for automatic optimization.
- 02Lazy loading and responsive resizing happen automatically.
- 03Specify width and height to prevent layout shift and improve scores.
Tips
- 01Always specify width and height — it prevents layout shift and improves performance significantly.
Warnings
- 01Don't use Image for images that change dimensions frequently — use regular img tags instead.
Next.js Image Optimization
(continued)Basic Image Component
- Import and use Image component from next/image.
import Image from 'next/image'; export default function Hero() { return ( <Image src="/hero.jpg" alt="Hero image" width={1200} height={600} /> ); } - Always specify width and height for best performance.
- alt text is required for accessibility.
- Images are served as WebP automatically for supported browsers.
- Use the placeholder prop to show a blur while the image loads.
import Image from 'next/image'; import heroImg from '@/public/hero.jpg'; <Image src={heroImg} alt="Hero" placeholder="blur" // works with static imports automatically /> - Use blurDataURL to provide a custom blur placeholder for external images.
<Image src="https://cdn.example.com/photo.jpg" alt="Photo" width={800} height={600} placeholder="blur" blurDataURL="data:image/png;base64,..." />
Next.js Image Optimization
(continued)Local vs External Images
- Local images in public folder.
import Image from 'next/image'; import heroImage from '@/public/hero.jpg'; <Image src={heroImage} alt="Hero" /> - Use remotePatterns to allowlist external image domains.
// next.config.js module.exports = { images: { remotePatterns: [{ hostname: "cdn.example.com", pathname: "/uploads/**" }] } }; - Match multiple domains with separate remotePatterns entries.
module.exports = { images: { remotePatterns: [ { hostname: "cdn.example.com" }, { hostname: "images.example.com", protocol: "https" } ] } }; - Static imports infer width, height, and enable blur placeholder automatically.
import logo from '@/public/logo.png'; // width/height inferred <Image src={logo} alt="Logo" /> - Use unoptimized to skip optimization for SVGs or animated GIFs.
<Image src="/icon.svg" alt="Icon" width={32} height={32} unoptimized />
Next.js Image Optimization
(continued)Responsive Images
- Use fill prop for responsive sizing.
<Image src="/hero.jpg" alt="Hero" fill style={{ objectFit: 'cover' }} /> - Specify sizes for responsive images.
<Image src="/card.jpg" alt="Card" width={300} height={200} sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw" /> - The fill image must be inside a positioned container.
<div style={{ position: "relative", width: "100%", height: "400px" }}> <Image src="/banner.jpg" alt="Banner" fill style={{ objectFit: "cover" }} /> </div> - Use objectPosition to control the focal point of a filled image.
<Image src="/portrait.jpg" alt="Portrait" fill style={{ objectFit: "cover", objectPosition: "top center" }} /> - Use sizes to help the browser choose the right srcset variant.
<Image src="/hero.jpg" alt="Hero" fill sizes="100vw" // full-width image />
Next.js Image Optimization
(continued)Lazy Loading
- Lazy loading is automatic by default.
<Image src="/below-fold.jpg" alt="Below fold image" width={800} height={600} loading="lazy" // Default behavior /> - Disable lazy loading for above-fold images.
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority // Load immediately /> - Use priority on the largest above-fold image to improve LCP score.
- Eager loading loads the image immediately without waiting for viewport.
<Image src="/logo.png" alt="Logo" width={120} height={40} loading="eager" // Don't defer /> - Images outside the viewport are deferred until the user scrolls near them.
<Image src="/infographic.png" alt="Infographic" width={800} height={600} // loading="lazy" is the default — no need to set it explicitly />
Next.js Image Optimization
(continued)Image Quality
- Control image quality with quality prop.
<Image src="/photo.jpg" alt="Photo" width={500} height={300} quality={80} // 1-100, default 75 /> - Format automatically optimized for browser.
// Automatically serves WebP to browsers that support it <Image src="/photo.jpg" alt="Photo" width={500} height={300} /> - Lower quality to 60–70 for hero images that cover large areas.
<Image src="/hero.jpg" alt="Hero" fill quality={65} /> // Good visual quality at smaller file sizes for large backgrounds - Configure global image quality defaults in next.config.js.
module.exports = { images: { qualities: [75, 90], // only generate these quality variants minimumCacheTTL: 86400 // cache optimized images for 1 day } }; - Use quality={100} for images where accuracy matters like product photos.
<Image src="/product-detail.png" alt="Product" width={600} height={600} quality={100} />
Next.js Image Optimization
(FAQ)FAQ
Yes — add the external hostname to the remotePatterns array in next.config.js under the images key. Without this, Next.js will block external image requests for security reasons.
Use the fill prop on the Image component and set the parent element to position: relative with a defined size. Then use sizes prop to hint the browser about the rendered width at different breakpoints for optimal file delivery.
Missing or incorrect sizes prop is the most common culprit — without it, Next.js serves a full-resolution image regardless of display size. Define sizes to match your layout breakpoints so the right image size is requested.
The default quality is 75, which balances file size and visual fidelity for most cases. Pass quality={90} or higher only for images where compression artifacts are noticeable, such as product photos or hero images.
Add the priority prop to images that appear above the fold — like hero banners or LCP candidates — so they are eagerly fetched instead of deferred. Only use it for the first few visible images; overusing it defeats the performance benefit of lazy loading.
Next.js Link and Navigation
Use the Link component and useRouter for fast client-side navigation in Next.js.
TL;DR
- 01Use Link for client-side navigation instead of <a> tags.
- 02Prefetch future pages automatically when links enter the viewport.
- 03Use useRouter hook to navigate programmatically after user actions.
Tips
- 01Always use Link for internal navigation — it enables better performance and a smoother user experience.
Warnings
- 01Avoid using <a> tags for internal links as they cause full page reloads and lose state.
Next.js Link and Navigation
(continued)Link Component
- Use Link for client-side navigation without page reload.
import Link from "next/link"; export default function Navigation() { return ( <nav> <Link href="/">Home</Link> <Link href="/about">About</Link> <Link href="/blog">Blog</Link> </nav> ); } - Link enables faster navigation and better performance.
- Only the needed data is fetched, not the whole page.
- Uses client-side rendering for instant transitions.
- Pass custom className or style props directly to Link.
<Link href="/contact" className="nav-link">Contact</Link>
Next.js Link and Navigation
(continued)Dynamic Links
- Create links with dynamic parameters.
import Link from "next/link"; export default function PostList({ posts }) { return ( <ul> {posts.map(post => ( <li key={post.id}> <Link href={`/blog/${post.slug}`}> {post.title} </Link> </li> ))} </ul> ); } - Use href with objects for typed routes.
<Link href={{ pathname: "/blog/[slug]", query: { slug: post.slug } }} > {post.title} </Link>
Next.js Link and Navigation
(continued)Prefetching
- Prefetch pages to load them in the background.
<Link href="/about" prefetch={true}> About </Link> - Prefetching is enabled by default for links in viewport.
- Disable prefetching for expensive operations.
<Link href="/api/heavy-data" prefetch={false}> Load Data </Link> - Manually prefetch with router.prefetch().
import { useRouter } from "next/navigation"; const router = useRouter(); function prefetchAbout() { router.prefetch("/about"); }
Next.js Link and Navigation
(continued)Programmatic Navigation
- Use useRouter for navigating with code.
import { useRouter } from "next/navigation"; export default function LoginForm() { const router = useRouter(); async function handleSubmit(e) { e.preventDefault(); // authenticate... router.push("/dashboard"); } return <form onSubmit={handleSubmit}>...</form>; } - Router methods: push, replace, back, forward.
router.push("/new-page"); // Add to history router.replace("/new-page"); // Replace history router.back(); // Go back router.forward(); // Go forward
Next.js Link and Navigation
(continued)Active Links
- Highlight current page in navigation.
"use client"; import { usePathname } from "next/navigation"; import Link from "next/link"; export default function Navigation() { const pathname = usePathname(); return ( <nav> <Link href="/" className={pathname === "/" ? "active" : ""} > Home </Link> <Link href="/about" className={pathname === "/about" ? "active" : ""} > About </Link> </nav> ); } - Use usePathname to get the current route.
- Compare pathname to set active classes.
Next.js Link and Navigation
(FAQ)FAQ
Use the useRouter hook from next/navigation and call router.push('/destination') after your async logic completes. For replacing history instead of pushing, use router.replace('/destination') to prevent users from navigating back.
Pass the interpolated path directly to Link's href: <Link href={/products/${product.id}}>. For complex routes, you can also pass an object: href={{ pathname: '/products/[id]', query: { id: product.id } }}.
Use the usePathname hook from next/navigation to get the current path, then compare it to the link's href to conditionally apply an active CSS class or style.
In production, Next.js prefetches linked pages when the Link enters the viewport, which speeds up navigation. You can disable this per-link with the prefetch={false} prop if the destination is rarely visited or costly to prefetch.
Next.js API Routes
Build and handle API endpoints directly inside your Next.js application.
TL;DR
- 01Create API routes in app/api folder with route.ts files.
- 02Export handler functions for HTTP methods (GET, POST, etc).
- 03Access request data and return typed Response objects with status codes.
Tips
- 01Use "use server" functions in app/actions for simple mutations — API routes are for complex endpoints.
Warnings
- 01API routes are public by default — add authentication checks for sensitive endpoints.
Next.js API Routes
(continued)Basic API Route
- Create API routes in app/api folder.
// app/api/hello/route.ts export async function GET(request: Request) { return Response.json({ message: 'Hello' }); } - Each route.ts file becomes an API endpoint.
- /app/api/hello/route.ts becomes /api/hello.
- Return a plain text response using the Response constructor.
export async function GET() { return new Response("Hello, world!", { headers: { "Content-Type": "text/plain" } }); } - Set response status and headers with the second argument.
export async function POST() { return Response.json({ created: true }, { status: 201 }); }
Next.js API Routes
(continued)HTTP Methods
- Handle different HTTP methods with exports.
// app/api/posts/route.ts export async function GET() { const posts = await fetchPosts(); return Response.json(posts); } export async function POST(request: Request) { const body = await request.json(); const post = await createPost(body); return Response.json(post, { status: 201 }); } - Supports GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.
- Handle PUT requests to update an existing resource.
export async function PUT(request: Request) { const body = await request.json(); const updated = await updatePost(body.id, body); return Response.json(updated); } - Handle DELETE to remove a resource by ID.
export async function DELETE(request: Request) { const { id } = await request.json(); await deletePost(id); return Response.json({ deleted: true }); } - Return 405 for unsupported methods.
export async function PATCH() { return Response.json({ error: "Method not allowed" }, { status: 405 }); }
Next.js API Routes
(continued)Request Handling
- Read JSON body from requests.
export async function POST(request: Request) { const body = await request.json(); console.log(body); return Response.json({ success: true }); } - Access query parameters and authorization headers.
export async function GET(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get('id'); const auth = request.headers.get('authorization'); return Response.json({ id, auth }); } - Parse form data from multipart requests.
export async function POST(request: Request) { const formData = await request.formData(); const name = formData.get("name") as string; return Response.json({ name }); } - Stream a large response body using ReadableStream.
export async function GET() { const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("chunk 1")); controller.close(); } }); return new Response(stream); } - Validate request body shape before processing.
const { name, email } = await request.json(); if (!name || !email) { return Response.json({ error: "name and email are required" }, { status: 400 }); }
Next.js API Routes
(continued)Cookies and Headers
- Read cookies from incoming requests —
cookies()is async in Next.js 15.import { cookies } from "next/headers"; export async function GET() { const cookieStore = await cookies(); // must await in Next.js 15 const token = cookieStore.get("token")?.value; return Response.json({ token }); } - Set cookies in the response.
import { cookies } from "next/headers"; export async function POST() { const cookieStore = await cookies(); cookieStore.set("session", "abc123", { httpOnly: true }); return Response.json({ ok: true }); } - Read request headers directly from the Request object.
export async function GET(request: Request) { const auth = request.headers.get("authorization"); const contentType = request.headers.get("content-type"); return Response.json({ auth, contentType }); } - Set custom response headers.
export async function GET() { return new Response(JSON.stringify({ ok: true }), { headers: { "Content-Type": "application/json", "Cache-Control": "no-store" } }); } - Use
next/headersheaders()to read request headers server-side (also async in Next.js 15).import { headers } from "next/headers"; const headersList = await headers(); const userAgent = headersList.get("user-agent");
Next.js API Routes
(continued)Error Handling and Status Codes
- Return appropriate status codes.
export async function GET(request: Request) { try { const data = await fetchData(); return Response.json(data); } catch (error) { return Response.json( { error: 'Failed to fetch data' }, { status: 500 } ); } } - Use Response.json with status and headers.
return Response.json(data, { status: 201, headers: { 'Content-Type': 'application/json' } }); - Return 400 for invalid or missing request body fields.
export async function POST(request: Request) { const body = await request.json(); if (!body.name) { return Response.json({ error: "Name is required" }, { status: 400 }); } return Response.json({ ok: true }); } - Return 401 for unauthenticated requests.
const token = request.headers.get("authorization"); if (!token) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } - Log errors server-side before returning a generic message.
} catch (error) { console.error("API error:", error); return Response.json({ error: "Internal server error" }, { status: 500 }); }
Next.js API Routes
(FAQ)FAQ
Create a file at app/api/[route]/route.ts and export named async functions matching HTTP methods (GET, POST, PUT, DELETE). Each function receives a Request object and must return a Response, e.g. return Response.json({ data }) or new Response(body, { status: 201 }).
Use new URL(request.url).searchParams to access query parameters, and await request.json() to parse a JSON body. For form data, use await request.formData() instead.
Name your folder with brackets, e.g. app/api/users/[id]/route.ts, then access the param via the second argument: export async function GET(request, { params }) { const { id } = await params; }.
Use Server Actions ("use server") for form submissions and simple data mutations tied to UI components — they require less boilerplate. Use API routes when you need a public HTTP endpoint, webhook receiver, or need full control over headers, status codes, and response shape.
Pass a status option to the Response constructor or Response.json(): return Response.json({ error: 'Not found' }, { status: 404 }). For errors, always set an explicit status code — omitting it defaults to 200 even on failure.
Next.js App Router
Master file-based routing with the App Router for modern Next.js apps.
TL;DR
- 01Create routes by organizing files in the app folder structure.
- 02Each page.tsx or page.jsx file becomes a route automatically.
- 03Use brackets [param] for dynamic routes like /blog/[slug].
Tips
- 01Use route groups (parentheses) to organize routes without affecting URLs — great for separating marketing and app sections.
Warnings
- 01Avoid deeply nested folder structures — keep routes 3–4 levels deep for clarity and maintainability.
Next.js App Router
(continued)Basic Routing
- Create pages by adding folders and page.tsx files.
app/ page.tsx # / route about/page.tsx # /about route blog/page.tsx # /blog route - Each folder represents a path segment in the URL.
- Rename page.tsx files to match your naming convention.
- Export a default React component from each page file.
// app/about/page.tsx export default function About() { return <h1>About Us</h1>; } - Use async functions in pages to fetch data on the server.
// app/blog/page.tsx export default async function Blog() { const posts = await getPosts(); return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>; }
Next.js App Router
(continued)Params and Static Generation
- Create dynamic routes with bracket syntax [param].
app/ blog/ [slug]/ page.tsx # /blog/:slug route - Access dynamic parameters — params is a Promise in Next.js 15, must be awaited.
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; return <h1>Post: {slug}</h1>; } - Use multiple dynamic segments for nested routes.
app/ users/[id]/posts/[postId]/page.tsx # /users/:id/posts/:postId - Generate static pages from dynamic params with generateStaticParams.
export async function generateStaticParams() { const posts = await getPosts(); return posts.map(p => ({ slug: p.slug })); } - Use notFound() to return a 404 when a resource is missing.
import { notFound } from "next/navigation"; export default async function Post({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const post = await getPost(slug); if (!post) notFound(); return <article>{post.content}</article>; }
Next.js App Router
(continued)Layouts
- Create shared layouts with layout.tsx files.
// app/layout.tsx export default function RootLayout({ children }) { return ( <html> <body> <Header /> {children} <Footer /> </body> </html> ); } - Each folder can have its own layout.
// app/blog/layout.tsx export default function BlogLayout({ children }) { return ( <div> <Sidebar /> {children} </div> ); } - Nested layouts wrap their children.
- Root layout must include html and body tags.
// app/layout.tsx — required at the root export default function RootLayout({ children }) { return ( <html lang="en"> <body>{children}</body> </html> ); } - Export metadata from a layout to share across child pages.
export const metadata = { title: "My Blog", description: "Posts about web development" };
Next.js App Router
(continued)Error and Loading Files
- Use error.tsx for error boundaries — it must be a Client Component.
// app/blog/error.tsx "use client"; export default function Error({ error, reset }) { return ( <div> <h2>Error loading blog</h2> <button onClick={() => reset()}>Try again</button> </div> ); } - Use loading.tsx for Suspense fallbacks.
// app/blog/loading.tsx export default function Loading() { return <p>Loading posts...</p>; } - Use not-found.tsx for 404 pages.
// app/blog/[slug]/not-found.tsx export default function NotFound() { return <h1>Post not found</h1>; } - Use template.tsx for re-mounting a layout on navigation.
// app/blog/template.tsx export default function Template({ children }) { return <div>{children}</div>; // Remounts on each navigation } - Use route.ts files to define API handlers inside the app router.
// app/api/hello/route.ts export async function GET() { return Response.json({ message: "Hello" }); }
Next.js App Router
(continued)Advanced Patterns
- Group routes with parentheses (don't affect URL).
app/ (marketing)/ page.tsx # / (still at root) about/page.tsx # /about (dashboard)/ layout.tsx page.tsx # /dashboard - Create catch-all routes with [...slug].
app/ docs/[...slug]/page.tsx # /docs/a/b/c - Use optional catch-all [[...slug]].
app/ docs/[[...slug]]/page.tsx # Matches /docs, /docs/a, /docs/a/b/c - Use parallel routes with @ to render multiple pages simultaneously.
app/ @analytics/ page.tsx @team/ page.tsx layout.tsx # Receives both slots as props - Use intercepting routes to show modals without full navigation.
app/ photos/ [id]/ page.tsx # /photos/42 — full page (.)photos/ [id]/ page.tsx # intercepted modal view
Next.js App Router
(FAQ)FAQ
Use [...slug] folder syntax to match multiple path segments — for example, app/docs/[...slug]/page.tsx matches /docs/a, /docs/a/b, and so on. In Next.js 15, params is a Promise: type it as Promise<{ slug: string[] }> and await it before accessing the slug array.
layout.tsx persists across route changes and does not remount, making it ideal for shared UI like navbars. template.tsx creates a new instance on every navigation, which is useful when you need effects or animations to re-run between pages.
Wrap the routes in a route group by naming the folder with parentheses, like (marketing) or (app). Files inside are grouped under that layout but the folder name is excluded from the URL.
Yes — nest dynamic folders like app/shop/[category]/[productId]/page.tsx to capture multiple params. In Next.js 15, type params as Promise<{ category: string; productId: string }> and use const { category, productId } = await params before accessing the values.
loading.tsx only triggers on the initial load of a segment, not on client-side navigations within the same layout boundary. Wrap slow data-fetching components in individual Suspense boundaries to get streaming loading states on every navigation.
Next.js Caching and Revalidation
Understand ISR, revalidatePath, revalidateTag, and caching strategies for optimal performance.
TL;DR
- 01Use revalidate to set cache time and update pages periodically.
- 02Use revalidatePath to manually update specific pages.
- 03Use revalidateTag for tag-based cache invalidation.
Tips
- 01Use tags for related content so you can invalidate all affected pages at once when content changes in your CMS or database.
Warnings
- 01Set appropriate revalidate times for your use case — too short defeats caching benefits, too long means stale content for users.
Next.js Caching and Revalidation
(continued)Static Generation with Revalidation
- Set a revalidate time to cache pages and periodically rebuild.
export const revalidate = 3600; // Revalidate every hour export default async function Page() { const data = await fetch("https://api.example.com/data"); return <div>{data}</div>; } - Pages are cached and served statically until the time expires.
- When the time expires, the next request rebuilds the page on demand.
- Great for content that changes infrequently but needs eventual freshness.
- revalidate is measured in seconds.
Next.js Caching and Revalidation
(continued)On-Demand Revalidation with revalidatePath
- Manually revalidate specific paths when data changes.
// app/api/revalidate/route.ts import { revalidatePath } from "next/cache"; export async function POST() { revalidatePath("/blog/[slug]"); revalidatePath("/blog"); return { revalidated: true }; } - Call this endpoint after updating content in your CMS or database.
- Paths are immediately regenerated on the next request.
- Pass exact paths or patterns for multiple routes.
revalidatePath("/blog/[slug]", "page"); // All dynamic blog posts revalidatePath("/", "layout"); // Entire site using this layout - Use webhooks from your CMS to trigger revalidation automatically.
Next.js Caching and Revalidation
(continued)Tag-Based Revalidation
- Use tags to group related cache entries for bulk invalidation.
// app/page.tsx export default async function Page() { const res = await fetch("https://api.example.com/posts", { next: { tags: ["posts"] } }); const posts = await res.json(); return <div>{posts}</div>; } - Revalidate all requests with a specific tag at once.
// app/api/revalidate/route.ts import { revalidateTag } from "next/cache"; export async function POST(request) { const tag = request.nextUrl.searchParams.get("tag"); revalidateTag(tag); // "posts" return { revalidated: true }; } - Useful for invalidating multiple related pages together.
// Invalidate all blog-related content revalidateTag("blog");
Next.js Caching and Revalidation
(continued)Fetch Cache Control
- Configure fetch caching behavior for API calls.
// Cache for 1 hour const res = await fetch("https://api.example.com/data", { next: { revalidate: 3600 } }); // Never cache const res = await fetch("https://api.example.com/data", { cache: "no-store" }); // Cache indefinitely (default) const res = await fetch("https://api.example.com/data", { next: { revalidate: false } }); - Fetch requests are cached by default in server components.
- Set no-store to always fetch fresh data on every request.
- Combine with tags for flexible cache invalidation.
Next.js Caching and Revalidation
(continued)Caching Strategies
- Static generation with periodic revalidation for blog posts.
export const revalidate = 86400; // Daily export async function generateStaticParams() { const posts = await getPosts(); return posts.map(p => ({ slug: p.slug })); } export default async function Post({ params }) { const post = await getPost(params.slug); return <article>{post.content}</article>; } - Dynamic rendering with revalidateTag for content that updates frequently.
export default async function Dashboard() { const data = await fetch("https://api.example.com/data", { next: { tags: ["dashboard"] } }); return <div>{data}</div>; } - Use manual revalidation with webhooks from your CMS.
- Choose based on how often content changes and performance needs.
Next.js Caching and Revalidation
(FAQ)FAQ
revalidatePath invalidates the cache for a specific URL or route segment, while revalidateTag invalidates all cached entries associated with a given tag across multiple routes. Use revalidateTag when a single content update (like a blog post edit) should purge several pages at once.
Call revalidatePath('/your-route') or revalidateTag('your-tag') inside a Route Handler (app/api/revalidate/route.ts). Protect the endpoint with a secret token by checking a query param or header against an environment variable before revalidating.
Pass { cache: 'no-store' } to fetch to always fetch fresh data, or { next: { revalidate: 0 } } for the same effect. For per-request caching without a time limit use { cache: 'force-cache' }, which is the default for static routes.
Yes — pass the interpolated path like revalidatePath('/posts/123') to purge a single dynamic page, or revalidatePath('/posts/[id]', 'page') to purge all pages matching that segment pattern at once.
generateStaticParams pre-builds specific dynamic routes at build time, while the revalidate option (set via fetch or route config) controls how long those static pages are served before being regenerated in the background. You typically combine both: generateStaticParams to build the initial set and revalidate to keep them fresh after deployment.
Next.js Compiler
Understand the Next.js SWC compiler, build options, and performance optimizations.
TL;DR
- 01Next.js uses SWC, a Rust-based compiler for speed.
- 02SWC produces faster builds and smaller bundles than Babel.
- 03Configure compiler transformations in next.config.js for production.
Tips
- 01Next.js optimizations are automatic — you don't need to do anything special. Just write good code and the compiler handles the rest.
Warnings
- 01Avoid manually configuring Babel unless absolutely necessary — SWC is faster and handles most cases.
Next.js Compiler
(continued)SWC Compiler
- Next.js uses SWC by default for faster compilation.
- 17x faster than Babel for transpilation.
- Supports all modern JavaScript features.
# No configuration needed - works out of the box npm run build - Built-in support for TypeScript and JSX.
- SWC handles dead code elimination automatically during production builds.
npm run build # Unused imports and exports are removed automatically - Check which compiler Next.js is using at build time.
next info # Shows SWC or Babel depending on your config
Next.js Compiler
(continued)Compiler Configuration
- Configure compiler options in next.config.js — SWC minification is enabled by default in Next.js 15.
// next.config.js module.exports = { compiler: { reactRemoveProperties: true, // Remove React debugging props removeConsole: { exclude: ['error', 'warn'] // Keep error and warn logs } } }; - Enable styled-components transform natively via SWC — replaces babel-plugin-styled-components.
module.exports = { compiler: { styledComponents: true } }; - Strip all console.log calls from the production build, keeping error and warn.
module.exports = { compiler: { removeConsole: { exclude: ['error', 'warn'] } } }; - Remove data-testid attributes to reduce production HTML size.
module.exports = { compiler: { reactRemoveProperties: { properties: ["^data-testid$"] } } }; - Enable emotion CSS-in-JS support through the SWC compiler.
module.exports = { compiler: { emotion: true } };
Next.js Compiler
(continued)Build Optimization
- Tree-shake large icon and component libraries with
optimizePackageImports.// next.config.js module.exports = { experimental: { optimizePackageImports: ["lucide-react", "@heroicons/react", "@mui/material"] } }; - Use
output: 'standalone'for minimal Docker images in production.module.exports = { output: "standalone" // Creates .next/standalone with no node_modules needed }; - Enable Turbopack for faster HMR and cold start in development (Next.js 15).
next dev --turbopack # Rust-based bundler: faster incremental builds than webpack - Delete
.babelrcto re-enable SWC if your project accidentally falls back to Babel.# Next.js uses Babel instead of SWC when .babelrc exists rm .babelrc # or migrate its config to next.config.js compiler options - Add bundle analyzer to inspect chunk composition per route.
npm install -D @next/bundle-analyzer ANALYZE=true npm run build # Opens interactive treemap
Next.js Compiler
(continued)Next.js-Specific Features
- Automatic image optimization during build.
- CSS-in-JS extraction and optimization.
- Automatic font optimization with next/font.
import { Inter } from 'next/font/google'; const inter = Inter(); - Server Actions are compiled with special serialization automatically.
"use server"; export async function save(data: FormData) { // Compiled to a secure server endpoint automatically } - TypeScript paths are resolved at compile time with no extra config.
{ "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
Next.js Compiler
(continued)Build Output and CI
- Review the build output table after every
npm run buildto catch First Load JS regressions.npm run build # Route Size First Load JS # / 5.2 kB 89.3 kB ← aim for < 130 kB - Wrap next.config.js with bundle analyzer to generate an interactive treemap.
const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: process.env.ANALYZE === "true" }); module.exports = withBundleAnalyzer({}); - Run Lighthouse CI in GitHub Actions to gate performance regressions.
- name: Lighthouse CI run: lhci autorun env: LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_TOKEN }} - Set a minimum Lighthouse performance score to fail CI on regressions.
{ "assert": { "assertions": { "categories:performance": ["error", { "minScore": 0.9 }] } } } - Use
next infoin the terminal to confirm whether SWC or Babel is active.next info # Operating System: ..., SWC: true
Next.js Compiler
(FAQ)FAQ
SWC can be up to 17x faster for local compilation and 5x faster for full production builds. The difference is most noticeable in large projects with hundreds of files, where Babel's JavaScript-based transforms become the bottleneck.
Add a compiler key in next.config.js — for example, compiler: { styledComponents: true } enables the styled-components transform natively via SWC. This replaces the need for babel-plugin-styled-components and works automatically in both dev and production.
Next.js automatically falls back to Babel when it detects a .babelrc or babel.config.js file in the project root. Delete these files (migrating any custom plugins to next.config.js compiler options) to re-enable SWC.
Use modularizeImports in next.config.js to rewrite bulk imports into per-file imports at compile time — for example, configuring it for @mui/icons-material prevents the entire icon library from being bundled. This is handled by the SWC compiler and requires no runtime code changes.
Install @next/bundle-analyzer and set ANALYZE=true next build to get an interactive treemap of your bundles. For raw build timing, the CLI output already reports per-page sizes and the First Load JS for each route after every build.
Next.js Components
Build Next.js components using server and client rendering strategies for performance.
TL;DR
- 01Server components render on server, no JavaScript sent.
- 02Client components (use client) render in browser with state.
- 03Combine both for optimal performance and interactivity.
Tips
- 01Default to server components for better performance — only use client components when you need interactivity.
Warnings
- 01"use client" at the file level makes the entire file client-side — keep client components minimal and separate.
Next.js Components
(continued)Server Component Patterns
- Server components are the default in App Router.
// app/components/BlogPost.tsx export default async function BlogPost({ slug }) { const post = await fetchPost(slug); return <article>{post.content}</article>; } - Can access databases, secrets, and APIs directly.
- No JavaScript sent to browser for these components.
- Cannot use hooks or browser APIs.
- Use async/await directly in the component to fetch data.
export default async function UserProfile({ id }) { const user = await db.user.findUnique({ where: { id } }); return <p>{user.name}</p>; } - Pass props from server components to client components safely.
export default async function Page() { const settings = await getSettings(); return <ThemeProvider theme={settings.theme} />; }
Next.js Components
(continued)Interactive Patterns
- Mark components to render on client with "use client".
"use client"; import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; } - Can use React hooks and browser APIs.
- Must declare at top of file.
- Use for interactive features.
- Use useEffect to run code only after the component mounts.
"use client"; import { useEffect, useState } from "react"; export function Clock() { const [time, setTime] = useState(""); useEffect(() => { setTime(new Date().toLocaleTimeString()); }, []); return <p>{time}</p>; } - Access browser APIs like localStorage inside client components.
"use client"; export function SaveButton({ data }) { function save() { localStorage.setItem("draft", JSON.stringify(data)); } return <button onClick={save}>Save Draft</button>; }
Next.js Components
(continued)Server and Client Composition
- Pass server components as children to client components.
"use client"; export default function Layout({ children }) { return <div>{children}</div>; } // children can be server components <Layout> <ServerComponent /> </Layout> - Keep sensitive data in server components, pass only safe props.
// Server component fetches and sanitizes data export default async function Page() { const post = await getPost(); // has private fields return <PostCard title={post.title} body={post.body} />; } - Use context providers at the root for shared client state.
"use client"; export function ThemeProvider({ children }) { const [theme, setTheme] = useState("light"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); } - Wrap only interactive parts in client components to minimize bundle.
// app/page.tsx (server) import LikeButton from "./LikeButton"; // only this is client export default async function Post() { const post = await getPost(); return ( <article> <h1>{post.title}</h1> <LikeButton postId={post.id} /> </article> ); } - Avoid importing server-only modules in client components.
import "server-only"; // throws if imported by a client bundle
Next.js Components
(continued)Data Fetching Patterns
- Fetch in server components for better performance.
export default async function Products() { const res = await fetch('/api/products'); const products = await res.json(); return ( <ul> {products.map(p => <li key={p.id}>{p.name}</li>)} </ul> ); } - Fetch from client for real-time or user-specific data.
"use client"; import { useEffect, useState } from "react"; export function LiveFeed() { const [data, setData] = useState(null); useEffect(() => { fetch('/api/live-data') .then(r => r.json()) .then(setData); }, []); return <div>{data?.message}</div>; } - Use SWR for client-side fetching with caching and revalidation — provide a fetcher that parses JSON.
"use client"; import useSWR from "swr"; const fetcher = (url: string) => fetch(url).then(r => r.json()); export function Profile({ id }) { const { data, error } = useSWR(`/api/users/${id}`, fetcher); if (error) return <p>Error</p>; return <p>{data?.name}</p>; } - Parallel fetch multiple server requests to reduce wait time.
export default async function Page() { const [user, posts] = await Promise.all([getUser(), getPosts()]); return <div><UserCard user={user} /><PostList posts={posts} /></div>; } - Deduplicate fetch calls with the built-in request memoization.
// Both components call getUser() — fetched only once per request async function Header() { const u = await getUser(); return <p>{u.name}</p>; } async function Sidebar() { const u = await getUser(); return <p>{u.role}</p>; }
Next.js Components
(continued)Component Organization
- Separate server and client concerns into different folders.
app/ components/ server/ BlogPost.tsx # Server component Header.tsx # Server component client/ Counter.tsx # Client component Modal.tsx # Client component - Keep client components small and focused.
"use client"; // Only interactive part is client export function Favorite({ postId }) { const [liked, setLiked] = useState(false); return <button onClick={() => setLiked(!liked)}>{liked ? "Liked" : "Like"}</button>; } - Colocate components near the pages that use them.
app/ blog/ page.tsx components/ PostCard.tsx PostList.tsx - Export reusable components from a shared ui folder.
// app/ui/Button.tsx export function Button({ children, onClick }) { return <button className="btn" onClick={onClick}>{children}</button>; } - Use index files to simplify imports from component folders.
// app/ui/index.ts export { Button } from "./Button"; export { Card } from "./Card"; // import { Button, Card } from "@/app/ui";
Next.js Components
(FAQ)FAQ
Use Client Components when you need browser APIs, event listeners, useState, or useEffect. If your component is purely for rendering data without interactivity, keep it as a Server Component to avoid shipping unnecessary JavaScript to the browser.
Yes — you can import and render Client Components inside Server Components. The boundary only goes one way: Client Components cannot import Server Components, but you can pass Server Components as children or props to Client Components.
The 'use client' directive marks a module boundary — everything imported by that file is also bundled for the client. Extract interactive logic into a small, focused component and add 'use client' only there to keep the rest of the tree server-rendered.
Directly call async functions or await fetch() inside the component body — Server Components are async by default. There's no need for useEffect or SWR; the data is fetched at render time on the server and the result is streamed to the client.
A layout wraps multiple pages and persists across navigation, while a page is the unique UI for a route segment. Both default to Server Components; only add 'use client' if the layout or page itself requires interactivity, otherwise keep data fetching server-side for better performance.
Next.js Data Fetching
Fetch data in Next.js using server components, static generation, ISR, and client fetching.
TL;DR
- 01Fetch data directly in server components with async/await.
- 02Use revalidate for static generation with periodic updates.
- 03ISR updates pages in background without full rebuild.
Tips
- 01Fetch data in server components whenever possible — it's faster and more secure than client-side fetching.
Warnings
- 01Don't fetch the same data multiple times — use fetch caching to reuse responses automatically.
Next.js Data Fetching
(continued)Server Component Data Fetching
- Fetch data directly in async server components.
export default async function Page() { const res = await fetch('https://api.example.com/data'); const data = await res.json(); return <div>{data.title}</div>; } - Data is fetched on the server, only HTML is sent to browser.
- Can access databases and secrets securely.
export default async function Products() { const products = await db.query("SELECT * FROM products"); return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>; } - Fetch multiple resources in parallel to reduce total wait time.
export default async function Dashboard() { const [user, orders] = await Promise.all([getUser(), getOrders()]); return <div><UserCard user={user} /><OrderList orders={orders} /></div>; } - Next.js deduplicates identical fetch calls within the same request.
// Both Header and Sidebar call getUser() — fetched only once async function Header() { const u = await getUser(); return <p>{u.name}</p>; } async function Sidebar() { const u = await getUser(); return <p>{u.role}</p>; } - Use error boundaries to handle failed fetch requests gracefully.
// app/blog/error.tsx catches errors thrown during fetching export default function Error({ reset }) { return <button onClick={reset}>Retry</button>; }
Next.js Data Fetching
(continued)Fetch Cache Configuration
- In Next.js 15, fetch is uncached by default — opt in to caching explicitly.
// Next.js 15 default: no caching — always fetches fresh data const res = await fetch('https://api.example.com/data'); // Opt in to caching: revalidate at most every hour const res = await fetch('https://api.example.com/data', { next: { revalidate: 3600 } }); - Use force-cache for data that should be cached indefinitely until manually invalidated.
const res = await fetch('https://api.example.com/static', { cache: 'force-cache' // Cached until revalidatePath/revalidateTag is called }); - Set revalidate at the route segment level to apply caching to the whole page.
export const revalidate = 86400; // Re-render at most every 24 hours export default async function Page() { const data = await fetchStaticData(); return <div>{data.content}</div>; } - Use next:{tags:[...]} to group cached fetches for tag-based invalidation.
const res = await fetch("https://api/posts", { next: { revalidate: 3600, tags: ["posts"] } }); // Call revalidateTag("posts") to invalidate all fetches tagged "posts" - Use unstable_cache to apply caching to non-fetch data like database queries.
import { unstable_cache } from "next/cache"; const getCachedUser = unstable_cache( async (id) => db.user.findUnique({ where: { id } }), ["user"], { revalidate: 3600 } );
Next.js Data Fetching
(continued)Parallel and Sequential Fetching
- Fetch multiple independent resources in parallel to avoid waterfall delays.
export default async function Dashboard() { // Both requests fire at the same time const [user, orders] = await Promise.all([ fetch("/api/user").then(r => r.json()), fetch("/api/orders").then(r => r.json()) ]); return <div><UserCard user={user} /><OrderList orders={orders} /></div>; } - Use sequential awaits only when later data depends on earlier results.
const user = await getUser(userId); const posts = await getPostsByAuthor(user.id); // depends on user.id - Wrap repeated data calls with React cache() so multiple components share one request.
import { cache } from "react"; export const getUser = cache(async (id: string) => fetch(`/api/users/${id}`).then(r => r.json()) ); // Header and Sidebar both call getUser(id) — only one HTTP request is made - Use Promise.allSettled to fetch multiple resources without short-circuiting on error.
const results = await Promise.allSettled([ getUser(id), getRecommendations(id) ]); const user = results[0].status === "fulfilled" ? results[0].value : null; - Suspend individual slow sections with Suspense so fast data renders first.
import { Suspense } from "react"; export default function Page() { return ( <div> <FastHeader /> <Suspense fallback=<p>Loading...</p>> <SlowRecommendations /> </Suspense> </div> ); }
Next.js Data Fetching
(continued)Dynamic Parameters with ISR
- Generate pages for multiple parameters statically.
export async function generateStaticParams() { const posts = await getPosts(); return posts.map(p => ({ slug: p.slug })); } export const revalidate = 86400; // 1 day export default async function Post({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; // params is a Promise in Next.js 15 const post = await getPost(slug); return <article>{post.content}</article>; } - Pre-renders all post pages at build time.
- Use dynamicParams to control behavior for unknown params.
export const dynamicParams = true; // default: render on-demand for new params // Set to false to return 404 for params not in generateStaticParams - Pass fallback data while a new page generates for the first time.
export const dynamic = "force-static"; // pre-render everything at build - Combine generateStaticParams with revalidation for hybrid pages.
export const revalidate = 3600; export async function generateStaticParams() { const featured = await getFeaturedPosts(); return featured.map(p => ({ slug: p.slug })); // Other slugs generate on first request, then are cached } - Use notFound() inside the page to handle deleted resources gracefully.
const { slug } = await params; const post = await getPost(slug); if (!post) notFound(); // Returns 404 instead of a broken page
Next.js Data Fetching
(continued)Client-Side Data Fetching
- Fetch data on client with useEffect.
"use client"; import { useEffect, useState } from "react"; export default function Component() { const [data, setData] = useState(null); useEffect(() => { fetch('/api/data') .then(r => r.json()) .then(setData); }, []); return <div>{data}</div>; } - Use for real-time data or user-specific content.
- Use SWR for built-in caching, revalidation, and error states — pass a fetcher that returns parsed JSON.
"use client"; import useSWR from "swr"; const fetcher = (url: string) => fetch(url).then(r => r.json()); export function Profile({ id }) { const { data, error, isLoading } = useSWR(`/api/users/${id}`, fetcher); if (isLoading) return <p>Loading...</p>; if (error) return <p>Error loading profile</p>; return <p>{data.name}</p>; } - Use React Query for more advanced caching and mutation workflows.
"use client"; import { useQuery } from "@tanstack/react-query"; export function Posts() { const { data } = useQuery({ queryKey: ["posts"], queryFn: fetchPosts }); return <ul>{data?.map(p => <li key={p.id}>{p.title}</li>)}</ul>; } - Prefer server components over client fetching when data isn't user-specific.
// Server component: no loading state, no useEffect needed export default async function PublicFeed() { const posts = await getPosts(); return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>; }
Next.js Data Fetching
(FAQ)FAQ
Mark the component async and use await with fetch directly in the component body — no useEffect or API routes needed. Next.js automatically deduplicates identical fetch calls within the same render cycle.
Static generation builds pages once at deploy time, while ISR (Incremental Static Regeneration) rebuilds individual pages in the background after a set revalidation interval without triggering a full rebuild. Use ISR when content changes periodically but doesn't need to be real-time.
Pass next: { revalidate: 60 } as a fetch option to regenerate the page at most every 60 seconds, or export const revalidate = 60 at the top of a page/layout file to apply it globally to that route.
Yes — export generateStaticParams to pre-render a subset of dynamic routes at build time, and any unvisited params will be generated on first request and then cached according to your revalidate setting.
Use client-side fetching (SWR or React Query) for data that changes based on user interaction, browser state, or requires real-time updates after the initial page load. Anything that can be fetched at request time without user context belongs in a Server Component.
Next.js Database Integration
Connect to databases using ORMs like Prisma, handle migrations, and query data safely.
TL;DR
- 01Use an ORM like Prisma to manage database connections safely.
- 02Define schemas and run migrations to evolve your database.
- 03Query data from server components or API routes securely.
Tips
- 01Always use Prisma client from a singleton instance to avoid connection pool exhaustion, especially in serverless environments.
Warnings
- 01Never expose database credentials in client code — all queries must happen on the server or through secure API routes.
Next.js Database Integration
(continued)Setting Up Prisma
- Install Prisma and initialize your project.
npm install @prisma/client npm install -D prisma npx prisma init - Set your database connection in the .env file.
DATABASE_URL="postgresql://user:password@localhost:5432/mydb" - Create a Prisma client instance for database queries.
// lib/prisma.ts import { PrismaClient } from "@prisma/client"; const globalForPrisma = global as unknown as { prisma: PrismaClient | undefined; }; export const prisma = globalForPrisma.prisma ?? new PrismaClient(); if (process.env.NODE_ENV !== "production") { globalForPrisma.prisma = prisma; } - This pattern prevents multiple Prisma instances in development.
Next.js Database Integration
(continued)Defining Schemas
- Define your database schema in schema.prisma.
// prisma/schema.prisma datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique name String? posts Post[] } model Post { id Int @id @default(autoincrement()) title String content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId Int } - Define relationships between models clearly.
- Use @unique for fields that should be unique.
- Use @default for default values at creation time.
Next.js Database Integration
(continued)Running Migrations
- Create a new migration after changing the schema.
npx prisma migrate dev --name add_posts - This creates a migration file and applies it to the database.
- Review the SQL and commit the migration file to version control.
# List all migrations npx prisma migrate status # Replay all migrations (for new databases) npx prisma migrate deploy - Never manually modify the database — use migrations.
- Migrations are essential for team collaboration and deployments.
Next.js Database Integration
(continued)Querying Data
- Query data from server components or API routes safely.
import { prisma } from "@/lib/prisma"; export default async function Users() { const users = await prisma.user.findMany(); return <div>{users.map(u => <div>{u.name}</div>)}</div>; } - Fetch a single record by ID or unique field.
const user = await prisma.user.findUnique({ where: { email: "alice@example.com" } }); - Create new records with validation.
const newUser = await prisma.user.create({ data: { email: "bob@example.com", name: "Bob" } }); - Update existing records safely.
const updated = await prisma.user.update({ where: { id: 1 }, data: { name: "Robert" } });
Next.js Database Integration
(continued)Common Patterns
- Include related data in queries to avoid N+1 problems.
const users = await prisma.user.findMany({ include: { posts: true } }); - Filter and sort results for complex queries.
const published = await prisma.post.findMany({ where: { published: true }, orderBy: { createdAt: "desc" }, take: 10 }); - Use transactions for multiple operations that must succeed together.
const [user, post] = await prisma.$transaction([ prisma.user.create({ data: { email: "test@example.com" } }), prisma.post.create({ data: { title: "Hello", authorId: 1 } }) ]); - Use API routes for mutations from client components.
// app/api/posts/route.ts export async function POST(request) { const data = await request.json(); const post = await prisma.post.create({ data }); return Response.json(post); }
Next.js Database Integration
(FAQ)FAQ
Export a single PrismaClient instance from a dedicated module and cache it on the global object during development to survive hot reloads. In production, one instance per serverless function lifecycle is fine as long as you're not instantiating it inside request handlers on every call.
Yes — Server Components run exclusively on the server, so you can import your Prisma client and call it directly without an API route. Just ensure you never pass the client or raw query results containing sensitive fields to Client Components.
Use migrate dev during local development — it generates migration files and applies them, and can reset the database if needed. Use migrate deploy in CI/CD and production — it applies existing migration files without generating new ones or altering data.
Add a seed script to prisma/seed.ts and register it under the prisma.seed key in package.json, then run npx prisma db seed. Prisma migrate dev will also call the seed script automatically after resetting the database.
Prisma handles most CRUD and relational queries cleanly, but for heavy aggregations or queries that generate inefficient SQL you can drop down to prisma.$queryRaw with tagged template literals, which still protects against SQL injection through parameterization.
Next.js Deployment
Deploy Next.js apps to production on Vercel, Docker, and other platforms.
TL;DR
- 01Run npm run build to compile your app for production.
- 02Deploy to Vercel with one command or by connecting a Git repo.
- 03Set environment variables in your deployment platform dashboard.
Tips
- 01Use Vercel for simplest deployment — it's optimized for Next.js and includes preview deployments.
Warnings
- 01Never commit .env.production files — use your deployment platform's secret management.
Next.js Deployment
(continued)Building for Production
- Build your app for production.
npm run build npm start - Build creates .next folder with optimized files.
- Test production build locally before deploying.
npm run build npm start # Visit http://localhost:3000 - Inspect the build output for page sizes and bundle weights.
npm run build # Route Size First Load JS # / 5 kB 87 kB # /blog/[slug] 3 kB 85 kB - Use output: 'standalone' to create a minimal deployment folder.
// next.config.js module.exports = { output: "standalone" // .next/standalone contains everything needed to run }; - Set NODE_ENV=production when running the server manually.
NODE_ENV=production node .next/standalone/server.js
Next.js Deployment
(continued)Vercel Deployment
- Deploy to Vercel (easiest option).
npm install -g vercel vercel - Connect Git repository for automatic deployments.
git push # Automatically deploys to Vercel - Vercel is created by Next.js team.
- Preview deployments are created automatically for every pull request.
- Set environment variables using the Vercel CLI.
vercel env add DATABASE_URL production vercel env add NEXT_PUBLIC_API_URL production - Override the build command in vercel.json if needed.
{ "buildCommand": "npm run build", "outputDirectory": ".next" }
Next.js Deployment
(continued)Docker Deployment
- Create Dockerfile for Docker deployment.
FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build EXPOSE 3000 CMD ["npm", "start"] - Build and run Docker image.
docker build -t my-app . docker run -p 3000:3000 my-app - Use multi-stage builds to reduce the final image size.
FROM node:18-alpine AS builder WORKDIR /app COPY . . RUN npm ci && npm run build FROM node:18-alpine AS runner COPY --from=builder /app/.next/standalone ./ CMD ["node", "server.js"] - Pass environment variables at container runtime.
docker run -p 3000:3000 -e DATABASE_URL=postgres://... my-app - Use docker-compose for local development with a database.
services: app: build: . ports: ["3000:3000"] db: image: postgres:15 environment: POSTGRES_PASSWORD: secret
Next.js Deployment
(continued)Environment Variables
- Set environment variables in deployment platform.
# Vercel dashboard vercel env add DATABASE_URL vercel env add NEXT_PUBLIC_API_URL - Use .env.production for local production testing.
# .env.production DATABASE_URL=postgresql://prod-db NEXT_PUBLIC_API_URL=https://api.prod.com - Prefix public variables with NEXT_PUBLIC_ to expose to the browser.
NEXT_PUBLIC_STRIPE_KEY=pk_live_abc123 DATABASE_URL=postgres://secret # server-only - Validate required env variables at startup to catch missing config early.
// lib/env.ts if (!process.env.DATABASE_URL) { throw new Error("DATABASE_URL is required"); } - Store secrets in your CI/CD platform and never commit them to Git.
# GitHub Actions env: DATABASE_URL: ${{ secrets.DATABASE_URL }}
Next.js Deployment
(continued)Performance Optimization
- Enable Image Optimization for faster images.
import Image from 'next/image'; <Image src="/photo.jpg" alt="Photo" width={800} height={600} priority /> - Use static exports for fully static sites with no server needed.
// next.config.js module.exports = { output: "export" // Creates an out/ folder you can host on any CDN }; - Enable compression for smaller HTTP responses.
module.exports = { compress: true // enabled by default in production }; - Configure CDN caching for static assets.
module.exports = { headers: async () => [ { source: "/_next/static/(.*)", headers: [{ key: "Cache-Control", value: "public, max-age=31536000, immutable" }] } ] }; - Run lighthouse audits against the deployed production URL.
npx lighthouse https://your-app.vercel.app --output html
Next.js Deployment
(FAQ)FAQ
Install the Vercel CLI and run vercel from your project root — it auto-detects Next.js and configures everything. Alternatively, connect your GitHub repo in the Vercel dashboard and every push deploys automatically with zero config.
It tells Next.js to produce a self-contained build folder with only the files needed to run, which is essential for Docker deployments since it dramatically reduces image size. Use it whenever you're containerizing your app rather than deploying to a managed platform.
Variables prefixed with NEXT_PUBLIC_ are inlined into the client-side JavaScript bundle at build time and are visible in the browser. Variables without that prefix are server-only and accessible exclusively in API routes, Server Components, and data-fetching functions like getServerSideProps.
The most common cause is missing environment variables — your .env.local file isn't deployed, so any variables it defines must be explicitly set in your platform's dashboard. Check that all variables your app reads at runtime are registered in the deployment environment, not just at build time.
Switch data-heavy pages from getServerSideProps to Incremental Static Regeneration (ISR) using revalidate so responses are served from the CDN cache instead of computed on every request. Also enable compress: true in next.config.js if your hosting platform doesn't handle gzip automatically.
Next.js Error Handling
Implement custom error pages, error boundaries, and global error handling strategies.
TL;DR
- 01Create error.tsx files for segment-level error boundaries.
- 02Create not-found.tsx for 404 pages.
- 03Handle errors in server actions and API routes explicitly.
Tips
- 01Place global-error.tsx in the app root but rely on segment-level error.tsx files for most recovery UI — they give users more context-specific error messages.
Warnings
- 01Never expose sensitive error details to users — log detailed errors server-side and show friendly messages to clients.
Next.js Error Handling
(continued)Error Boundaries with error.tsx
- Create error.tsx for segment-level error handling.
// app/blog/error.tsx "use client"; import { useEffect } from "react"; export default function Error({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { useEffect(() => { console.error(error); }, [error]); return ( <div> <h2>Something went wrong!</h2> <button onClick={() => reset()}>Try again</button> </div> ); } - Error boundaries catch errors from child segments.
- Reset function allows users to retry.
- Must be a client component with "use client" directive.
- Nest error.tsx at each route level for targeted recovery — an error in /dashboard won't break the nav.
app/ error.tsx # catches app-level errors dashboard/ error.tsx # catches only dashboard errors settings/ error.tsx # catches only settings errors
Next.js Error Handling
(continued)Custom 404 Pages
- Create not-found.tsx for missing resources.
// app/blog/[slug]/not-found.tsx export default function NotFound() { return ( <div> <h1>Post not found</h1> <p>The post you're looking for doesn't exist.</p> </div> ); } - Use notFound() function to trigger the not-found page.
import { notFound } from "next/navigation"; export default async function Post({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const post = await getPost(slug); if (!post) { notFound(); } return <article>{post.content}</article>; } - Each segment can have its own not-found.tsx.
- Add a root-level not-found.tsx for app-wide 404 handling.
// app/not-found.tsx import Link from "next/link"; export default function RootNotFound() { return ( <div> <h1>Page not found</h1> <Link href="/">Go home</Link> </div> ); } - Export metadata from not-found.tsx for SEO.
export const metadata = { title: "404 — Page not found", description: "This page does not exist." };
Next.js Error Handling
(continued)Server Action Error Handling
- Catch and handle errors in server actions.
"use server"; export async function createPost(formData: FormData) { try { const title = formData.get("title") as string; if (!title) { return { error: "Title is required" }; } const post = await db.post.create({ data: { title } }); return { success: true, post }; } catch (error) { return { error: "Failed to create post" }; } } - Return error objects from server actions.
- Handle errors in client components.
"use client"; export default function Form() { const [error, setError] = useState<string | null>(null); async function handleSubmit(formData: FormData) { const result = await createPost(formData); if (result.error) { setError(result.error); } } return ( <form action={handleSubmit}> {error && <p>{error}</p>} </form> ); } - Use
useActionStateto wire server action errors directly into a form — the action receivesprevStateas its first argument (React 19 / Next.js 15).// actions.ts "use server"; export async function createPost(prevState: unknown, formData: FormData) { try { await db.post.create({ data: { title: formData.get("title") } }); return { success: true }; } catch { return { error: "Failed to create post" }; } } // Form.tsx "use client"; import { useActionState } from "react"; export default function Form() { const [state, action] = useActionState(createPost, null); return ( <form action={action}> {state?.error && <p>{state.error}</p>} <button type="submit">Create</button> </form> ); } - Log the full error server-side and return only a safe message to the client — never expose stack traces or database details.
"use server"; export async function deletePost(id: string) { try { await db.post.delete({ where: { id } }); return { success: true }; } catch (error) { console.error("[deletePost] failed:", error); // full error server-side only return { error: "Could not delete post" };// safe message to client } }
Next.js Error Handling
(continued)API Route Error Handling
- Return error responses with appropriate status codes.
// app/api/posts/[id]/route.ts export async function GET( request: Request, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; try { const post = await getPost(id); if (!post) { return Response.json( { error: "Post not found" }, { status: 404 } ); } return Response.json(post); } catch (error) { return Response.json( { error: "Internal server error" }, { status: 500 } ); } } - Always handle errors in API routes explicitly.
- Return meaningful status codes and error messages.
- Validate request body fields and return 400 for bad input.
export async function POST(request: Request) { const body = await request.json(); if (!body.title) { return Response.json({ error: "Title is required" }, { status: 400 }); } return Response.json({ ok: true }); } - Return 401 for missing or invalid authentication tokens.
const token = request.headers.get("authorization"); if (!token || !isValid(token)) { return Response.json({ error: "Unauthorized" }, { status: 401 }); }
Next.js Error Handling
(continued)Global Error Handling
Create
app/global-error.tsxto catch errors in the root layout — it replaces the layout entirely, so it must render<html>and<body>tags.// app/global-error.tsx — replaces root layout; MUST include <html><body> "use client"; export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { return ( <html> <body> <h1>Application Error</h1> <button onClick={reset}>Try again</button> </body> </html> ); }Regular
error.tsxrenders inside the existing layout — never add<html>or<body>tags there.// app/dashboard/error.tsx — renders inside layout, no html/body tags "use client"; export default function Error({ reset }: { reset: () => void }) { return <div><p>Something went wrong.</p><button onClick={reset}>Retry</button></div>; }Use the error
digestto correlate client errors with server log entries.export default function Error({ error }: { error: Error & { digest?: string } }) { return <p>Error ID: {error.digest}</p>; // matches server-side error log }Add error monitoring in the
useEffectof any error boundary component.useEffect(() => { Sentry.captureException(error); console.error("digest:", error.digest); }, [error]);Nest
error.tsxfiles at each route segment for targeted recovery — a dashboard error won't break the nav or sidebar.app/ error.tsx ← catches app-level errors (inside root layout) global-error.tsx ← catches root layout errors (replaces layout) dashboard/ error.tsx ← catches only dashboard errors
Next.js Error Handling
(FAQ)FAQ
error.tsx creates a React error boundary that catches rendering errors in a route segment and its children, while try/catch handles runtime errors in async server components or server actions. Use both: error.tsx for UI recovery, try/catch for data-fetching logic.
Place error.tsx files at each route segment level where you want isolated error recovery — for example, app/dashboard/error.tsx catches errors only in the dashboard segment without breaking the rest of the layout. Nesting them gives users more targeted recovery options.
Return a Response with an appropriate HTTP status code and a JSON body: return Response.json({ error: 'Not found' }, { status: 404 }). Always use 4xx for client errors and 5xx for server errors so consuming clients can handle them correctly.
Wrap your server action logic in a try/catch and return a structured result object like { success: false, error: 'message' } rather than rethrowing, since uncaught server action errors surface as generic failures to the client. Use the error only to inform the UI, never to expose stack traces or DB details.
Add a not-found.tsx file in the app directory (or a specific route segment) and call the notFound() function from 'next/navigation' anywhere in a server component to trigger it. This replaces the default Next.js 404 page for that segment.
Next.js Internationalization
Implement i18n in Next.js with routing, translations, locale detection, and multi-language support.
TL;DR
- 01Use locale-prefixed routing like /en, /es, /fr in the app directory.
- 02Organize translation files by locale and reference them in components.
- 03Detect locale from URL, user preference, or browser language.
Tips
- 01Use next-intl library for complex i18n needs like pluralization, date formatting, and locale-aware routing, as it handles many edge cases automatically.
- 02Use next-intl's <code>createNavigation</code> helpers — <code>Link</code>, <code>redirect</code>, and <code>usePathname</code> — instead of <code>next/link</code> directly so navigation always stays locale-aware.
Warnings
- 01Always set the HTML lang attribute correctly for each locale, since screen readers and search engines rely on it for language detection.
- 02Forgetting to include locale-prefixed paths in your middleware <code>matcher</code> config can cause static assets and API routes to get caught in a redirect loop.
Next.js Internationalization
(continued)Locale-Based Routing
- One dynamic segment can serve every language on your site — nest all routes inside a
[locale]folder to organize them by language.app/ [locale]/ layout.tsx page.tsx about/ page.tsx - This creates URLs like
/en,/es,/en/about,/es/about, etc. - Access the locale parameter in your layouts and pages —
paramsis an asyncPromisethat must be awaited.export default async function Layout({ children, params }: { children: React.ReactNode; params: Promise<{ locale: string }>; }) { const { locale } = await params; return <html lang={locale}>{children}</html>; } - Use middleware to redirect to the user's preferred locale.
- Generate static params for all supported locales.
Next.js Internationalization
(continued)Translation Files
- Hardcoded strings scattered across components are a rewrite waiting to happen — centralize them in JSON files, one per locale.
locales/ en.json es.json fr.json - Structure translations hierarchically for nested content.
{ "common": { "welcome": "Welcome", "goodbye": "Goodbye" }, "home": { "title": "Home Page", "description": "Welcome to our site" } } - Load translations based on the current locale in your component.
import translations from "@/locales/en.json"; const message = translations.common.welcome; - Use a translation library like
next-intlfor complex needs.
Next.js Internationalization
(continued)Using the next-intl Library
- Rolling your own locale detection, pluralization, and date formatting adds up fast — next-intl bundles all three in one package.
npm install next-intl - Create a configuration file for supported locales and messages.
import { getRequestConfig } from "next-intl/server"; export default getRequestConfig(async ({ requestLocale }) => { const locale = await requestLocale; return { locale, messages: (await import(`./messages/${locale}.json`)).default }; }); - Use the
useTranslationshook to access translations in components."use client"; import { useTranslations } from "next-intl"; export default function HomePage() { const t = useTranslations("home"); return <h1>{t("title")}</h1>; } - next-intl handles formatting, pluralization, and date localization.
Next.js Internationalization
(continued)Locale Detection
- A browser sends its preferred language on every request — read the Accept-Language header in
middleware.tsto redirect users to the right locale automatically.// middleware.ts import { NextRequest, NextResponse } from "next/server"; export function middleware(request: NextRequest) { const { pathname } = request.nextUrl; const acceptLang = request.headers.get("accept-language") ?? "en"; const locale = acceptLang.split(",")[0].split("-")[0]; const supported = ["en", "es", "fr"]; const chosen = supported.includes(locale) ? locale : "en"; if (!pathname.startsWith(`/${chosen}`)) { return NextResponse.redirect(new URL(`/${chosen}${pathname}`, request.url)); } return NextResponse.next(); } - Check a locale cookie first so returning users keep their saved preference.
const cookieLocale = request.cookies.get("locale")?.value; const chosen = (cookieLocale && supported.includes(cookieLocale)) ? cookieLocale : detectedLocale; - Allow users to manually select their language and store it in a cookie.
"use client"; import { useRouter } from "next/navigation"; export function LocaleSwitcher({ current }: { current: string }) { const router = useRouter(); const switchLocale = (locale: string) => { document.cookie = `locale=${locale};path=/`; router.push(`/${locale}`); }; return ( <select value={current} onChange={(e) => switchLocale(e.target.value)}> <option value="en">English</option> <option value="es">Español</option> <option value="fr">Français</option> </select> ); } - Always validate locale values against an allowlist to prevent open redirect attacks.
- Use
useLocale()from next-intl to read the active locale in client components.
Next.js Internationalization
(continued)SEO and Hreflang
- Screen readers and search engines both rely on one attribute to know what language they're reading — set
langon the HTML element per locale.<html lang={locale}> - Add hreflang links to alternate language versions for SEO.
export async function generateMetadata({ params }) { const alternates = { languages: { en: `https://example.com/en/page`, es: `https://example.com/es/page`, fr: `https://example.com/fr/page` } }; return { alternates }; } - Generate sitemaps for each locale to help search engines index them.
- Use canonical URLs to prevent duplicate content issues.
Next.js Internationalization
(FAQ)FAQ
Create a [locale] dynamic segment at the root of your app directory (e.g., app/[locale]/page.tsx) and configure supported locales in middleware.ts to redirect users and validate locale params. This gives you clean URLs like /en/about and /fr/about automatically.
Use next-intl when you need pluralization rules, number/date formatting, or ICU message syntax, since the built-in Next.js i18n only handles routing. next-intl also provides React hooks like useTranslations that make accessing nested keys and interpolating variables much cleaner.
By default, Next.js detects locale from the Accept-Language request header and redirects accordingly. You can override this in middleware.ts by reading cookies, session data, or custom headers before the redirect, giving you full control over the detection priority.
Keep one JSON file per locale per namespace under a messages or locales directory (e.g., messages/en.json, messages/es.json) and split by feature or page to avoid loading all strings upfront. With next-intl, pass the relevant namespace to unstable_setRequestLocale and useTranslations('namespace') to scope your keys.
Export a generateMetadata function in each page and return an alternates.languages object mapping each locale to its full URL (e.g., { en: 'https://example.com/en/page', es: 'https://example.com/es/page' }). Next.js renders these as tags in the
automatically.Next.js Metadata and SEO
Generate metadata, open graph tags, and improve SEO automatically.
TL;DR
- 01Export metadata constant to set page titles and descriptions.
- 02Use generateMetadata for dynamic metadata from data.
- 03Use Open Graph tags for social media sharing.
Tips
- 01Use generateMetadata with dynamic data to create unique, SEO-friendly titles and descriptions for each page.
Warnings
- 01Always include Open Graph images with correct dimensions (1200x630) to ensure proper display on social media.
Next.js Metadata and SEO
(continued)Static Metadata
- Export metadata constant for static pages.
import { Metadata } from "next"; export const metadata: Metadata = { title: "About Us", description: "Learn more about our company and mission.", keywords: ["about", "company", "mission"] }; export default function About() { return <div>About content</div>; } - Metadata sets HTML head tags automatically.
- Improves SEO and social media sharing.
- Use a layout-level metadata object to apply defaults across all pages.
// app/layout.tsx export const metadata: Metadata = { title: { default: "My Site", template: "%s | My Site" }, description: "Default description for all pages." }; - Child page metadata merges with and overrides layout metadata.
// app/about/page.tsx export const metadata: Metadata = { title: "About" // becomes "About | My Site" via template };
Next.js Metadata and SEO
(continued)Dynamic Metadata
- Generate metadata from data or parameters.
import { Metadata } from "next"; export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> { const { slug } = await params; // params is a Promise in Next.js 15 const post = await getPost(slug); return { title: post.title, description: post.excerpt, authors: [{ name: post.author }] }; } export default async function Post({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const post = await getPost(slug); return <article>{post.content}</article>; } - generateMetadata runs on the server.
- Has access to params and other request data.
- Fetch and cache data inside generateMetadata to avoid duplicate requests.
export async function generateMetadata({ params }): Promise<Metadata> { const product = await getProduct(params.id); // Next.js deduplicates this fetch return { title: product.name, description: product.description }; } export default async function Page({ params }) { const product = await getProduct(params.id); // same fetch, cached return <ProductDetail product={product} />; } - Return notFound() inside generateMetadata to trigger a 404 page early.
export async function generateMetadata({ params }): Promise<Metadata> { const post = await getPost(params.slug); if (!post) notFound(); return { title: post.title }; }
Next.js Metadata and SEO
(continued)Open Graph Tags
- Set Open Graph tags for social sharing.
export const metadata: Metadata = { title: "My Post", description: "Read my latest blog post", openGraph: { title: "My Post", description: "Read my latest blog post", url: "https://example.com/blog/my-post", siteName: "My Blog", images: [ { url: "https://example.com/og-image.png", width: 1200, height: 630 } ], type: "article" } }; - Open Graph improves how links look on social media.
- Include images for better engagement.
- Use dynamic OG images with Next.js ImageResponse for per-page previews.
// app/og/route.tsx import { ImageResponse } from "next/og"; export async function GET(req: Request) { const { searchParams } = new URL(req.url); return new ImageResponse(<div>{searchParams.get("title")}</div>); } - Reference the dynamic OG route in your metadata image field.
openGraph: { images: [`/og?title=${encodeURIComponent(post.title)}`] }
Next.js Metadata and SEO
(continued)Twitter Card Tags
- Add Twitter Card metadata for tweets.
export const metadata: Metadata = { title: "My Post", description: "Read my latest blog post", twitter: { card: "summary_large_image", title: "My Post", description: "Read my latest blog post", images: ["https://example.com/og-image.png"], creator: "@myhandle" } }; - Twitter Cards make tweets more engaging.
- Use summary_large_image for best results.
- Set twitter.site to your handle for attribution on shared links.
twitter: { card: "summary_large_image", site: "@mycompany", creator: "@authorhandle" } - Reuse the same image URL for both OG and Twitter to reduce duplication.
const ogImage = "https://example.com/og-image.png"; export const metadata: Metadata = { openGraph: { images: [ogImage] }, twitter: { images: [ogImage] } };
Next.js Metadata and SEO
(continued)Structured Data
- Add JSON-LD structured data for search engines.
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const post = await getPost(slug); const schema = { "@context": "https://schema.org", "@type": "BlogPosting", headline: post.title, description: post.excerpt, image: post.image, author: { "@type": "Person", name: post.author }, datePublished: post.date }; return ( <> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} /> ); } - Structured data helps search engines understand content.
- Use schema.org types for consistency.
- Add BreadcrumbList schema to help search engines show site hierarchy.
const breadcrumb = { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: [ { "@type": "ListItem", position: 1, name: "Home", item: "/" }, { "@type": "ListItem", position: 2, name: "Blog", item: "/blog" } ] }; - Validate structured data with Google's Rich Results Test tool before deploying.
Next.js Metadata and SEO
(FAQ)FAQ
Export an async generateMetadata function from your page component that receives params and searchParams, fetches any needed data, and returns a metadata object with title and description fields. This runs server-side and generates unique metadata per route.
The exported metadata constant is for static values known at build time, while generateMetadata is an async function for values that depend on dynamic data like route params or database fetches. Use metadata when the values never change, generateMetadata when they do.
Include an openGraph object inside your metadata export or generateMetadata return, with fields like title, description, url, and images. The images array should contain objects with url, width: 1200, and height: 630 for correct social previews.
Render a <script type='application/ld+json'> tag inside your page component using JSX, with dangerouslySetInnerHTML set to __html: JSON.stringify(yourSchemaObject). Place it in the component's return alongside your content, not inside the metadata export.
Yes, add a twitter object to your metadata with fields like card, title, description, and images. If omitted, many platforms fall back to Open Graph values, but explicitly setting twitter gives you control over how your content appears specifically on X/Twitter.
Next.js Middleware
Use Next.js middleware for redirects, auth checks, headers, and request rewriting in the App Router.
TL;DR
- 01Create a middleware.ts file at the project root to intercept requests.
- 02Use middleware to check auth, redirect, or modify headers.
- 03Middleware runs in the Edge Runtime — keep logic fast, Node.js built-ins are unavailable.
Tips
- 01Use the <code>matcher</code> config to narrow which routes trigger middleware, since it runs before every request and impacts performance.
- 02Next.js 16 renames <code>middleware.ts</code> to <code>proxy.ts</code> going forward — the old filename still works but is deprecated, so plan to migrate.
Warnings
- 01Middleware runs for every request, so expensive operations will slow your app down — keep logic fast and simple.
- 02Middleware also runs on every matched prefetch, not just full navigations, so expensive checks can fire far more often than you expect.
Next.js Middleware
(continued)File and Runtime Setup
- Want to run logic before every route renders, with zero changes to your pages? Create one
middleware.tsfile at the root of your project, next to theappdirectory.// middleware.ts import { NextRequest, NextResponse } from "next/server"; export function middleware(request: NextRequest) { return NextResponse.next(); } - Middleware runs on every request before it reaches your routes.
- Use the
matcherexport to specify which paths trigger the middleware.export const config = { matcher: ['/api/:path*', '/admin/:path*'] }; - Return
NextResponse.next()to continue to the next handler. - Middleware runs in the Edge Runtime — a lightweight V8-based environment, not full Node.js. Most Node.js built-in modules (fs, path, crypto) are unavailable.
Next.js Middleware
(continued)Authentication Checks
- Skip writing auth checks into every protected page — check for a token in cookies once, in middleware, and redirect if it's missing.
export function middleware(request: NextRequest) { const token = request.cookies.get('auth')?.value; if (!token && request.nextUrl.pathname.startsWith('/admin')) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); } - Verify JWT tokens or check session validity before route access.
- Redirect unauthenticated users to a login page automatically.
- Use middleware to protect entire route segments without code in each page.
- Combine with headers to check authentication across the app.
Next.js Middleware
(continued)Redirects and Rewrites
- A redirect changes the URL bar, a rewrite doesn't — send users to a different URL using
NextResponse.redirect()when you want that visible change.if (request.nextUrl.pathname === '/old-page') { return NextResponse.redirect(new URL('/new-page', request.url)); } - Rewrite the request internally without changing the user's URL.
if (request.nextUrl.pathname.startsWith('/api')) { return NextResponse.rewrite(new URL('/api-v2/handler', request.url)); } - Use redirects for public URL changes and SEO-friendly migrations.
- Use rewrites to hide internal routing or proxy external APIs.
- Both preserve the original URL in the browser when rewriting.
Next.js Middleware
(continued)Custom Headers
- Need a security header on every single response without touching each route handler? Add or modify headers once in middleware.
export function middleware(request: NextRequest) { const response = NextResponse.next(); response.headers.set('X-Custom-Header', 'value'); return response; } - Add security headers like CORS, CSP, or rate-limiting headers.
- Read incoming headers to customize behavior based on the request.
const userAgent = request.headers.get('user-agent'); - Middleware runs before routes see the request, so headers apply everywhere.
- Use response headers to control caching and security policies.
Next.js Middleware
(continued)Geolocation and Advanced Patterns
- Middleware can branch on more than the URL path — check locale or country from the request and redirect to the right version.
const locale = request.headers.get('accept-language')?.split(',')[0].split('-')[0]; if (locale === 'es') { return NextResponse.redirect(new URL('/es' + request.nextUrl.pathname, request.url)); } request.geoandrequest.ipwere removed fromNextRequestin Next.js 15 — use thegeolocation()helper from@vercel/functionsto route based on the user's country instead.import { geolocation } from '@vercel/functions'; export function middleware(request: NextRequest) { const { country } = geolocation(request); if (country === 'DE') { return NextResponse.redirect(new URL('/de', request.url)); } }- Rate limit requests based on IP address or user ID — use
ipAddress()from@vercel/functionsnow thatrequest.ipis gone.import { ipAddress } from '@vercel/functions'; const ip = ipAddress(request) ?? request.headers.get('x-forwarded-for') ?? 'unknown'; // Check rate limit store keyed by ip... - Combine multiple checks in sequence for layered security.
- Keep middleware logic fast — it runs on the Edge Runtime before every matched request and adds latency proportional to its execution time.
Next.js Middleware
(FAQ)FAQ
Place middleware.ts (or middleware.js) in the root of your project, at the same level as your app or pages directory. Only one middleware file is supported per project.
Export a matcher config from your middleware file to target specific paths, e.g. export const config = { matcher: ['/dashboard/:path*', '/api/protected/:path*'] }. Without a matcher, middleware runs on every single request including static assets.
Read your auth token from cookies or headers using request.cookies.get() or request.headers.get(), then return NextResponse.redirect(new URL('/login', request.url)) if the check fails.
Yes — use NextResponse.next() and call response.headers.set('X-Custom-Header', 'value') before returning it. This is useful for adding security headers like Content-Security-Policy or X-Frame-Options globally.
A redirect sends the browser to a new URL (visible in the address bar), while a rewrite proxies the request to a different path internally without changing the URL the user sees. Use NextResponse.rewrite() for URL masking and NextResponse.redirect() when you want the client to navigate.
Next.js Server Components
A practical guide to server components, the use client directive, and rendering boundaries.
TL;DR
- 01Server components run on the server and send HTML to the browser.
- 02Use client components with the "use client" directive for interactivity.
- 03Mix both types to optimize performance and security.
Tips
- 01Keep the "use client" boundary as low as possible in the tree to maximize server rendering and minimize the client bundle size.
- 02Use the <code>server-only</code> and <code>client-only</code> packages together so the build fails fast if a module crosses the wrong boundary.
Warnings
- 01Never include environment secrets in client components, even as props, since they become visible in the browser bundle and HTML.
- 02Adding <code>"use client"</code> too high in the tree makes every component it imports client-side too, silently bloating the JavaScript bundle.
Next.js Server Components
(continued)Rendering Model
- A Server Component never sends JSX or markdown to the browser — React serializes its output into a streamable RSC payload, not regular JSON.
// app/page.tsx — runs entirely on the server, no client JS for this tree export default async function Page() { const post = await db.post.findFirst(); return <article>{post.title}</article>; } - The payload uses the React Flight protocol: each chunk maps to one resolved component, so the client renders pieces as they arrive instead of waiting for the whole tree.
- Client Components referenced inside the payload show up as module references the browser fetches separately, keeping server-only code out of that bundle.
- Because rendering happens once on the server, Server Components can be
asyncfunctions andawaitdata directly — there is no hydration step for their output. - Nothing in a Server Component re-executes in the browser, so state, refs, and lifecycle hooks have no meaning there.
Next.js Server Components
(continued)Server vs Client Tradeoffs
- Every component starts as a Server Component until something forces it to the client — so the real question is what justifies that cost, not how to flip the switch.
// Stays a Server Component: no interactivity, no hooks needed export default async function PriceTag({ productId }) { const price = await getPrice(productId); return <span>${price}</span>; } - Choose a Client Component only when the UI needs
useState,useEffect, event handlers, or browser-only APIs likelocalStorage. - Server Components shrink the JavaScript bundle because their code and dependencies never ship — a heavy markdown parser or date library used only on the server costs the client nothing.
- Server Components can read environment secrets and query databases directly; Client Components can only see what is explicitly passed to them as props.
- A page can mix both freely — the tradeoff is per-component, not per-page, so isolate interactivity into the smallest possible Client Component.
// Only LikeButton ships JS; the rest of the page stays server-rendered import LikeButton from "./LikeButton"; // "use client" lives inside this file export default async function Post({ id }) { const post = await getPost(id); return <article>{post.body}<LikeButton postId={id} /></article>; }
Next.js Server Components
(continued)Composition Boundaries
- A Client Component can render a Server Component without importing it — pass it through
childrenor another prop instead.// ClientShell.tsx ("use client") never imports ServerSidebar directly export default function ClientShell({ children }) { return <div className="shell">{children}</div>; } // app/page.tsx (Server Component) composes them together <ClientShell><ServerSidebar /></ClientShell> - The nested Server Component still renders on the server first — only its finished output crosses into the Client Component as a slot.
- A Client Component can never
importa Server Component file directly; that import would pull server code into the client module graph and fail to build. - Props flowing from a Server Component into a Client Component must be plain serializable values: strings, numbers, arrays, dates, and plain objects.
// OK — every value below survives the server-to-client boundary <ClientChart data={rows} title="Sales" updatedAt={new Date()} /> // Not OK — functions and class instances cannot cross as props // <ClientChart onSort={sortRows} /> - This children-as-slot pattern is the main way teams keep a heavy data-fetching tree server-rendered while still nesting it inside an interactive shell.
Next.js Server Components
(continued)Protecting Server-Only Code
- A Server Component's database calls and API keys are invisible to the browser by default — but only if nothing pulls that module into a Client Component's import graph.
import "server-only"; // Any client-side import of this file now fails at build time, not runtime export async function getSecretData() { return db.query("SELECT * FROM secrets"); } - A single accidental import of a server-only utility from a Client Component silently ships that code, and anything it touches, to every visitor's browser.
- The
server-onlypackage converts that silent leak into a build error, so the mistake is caught in CI instead of in production. - Pair it with
client-onlyin browser-specific utilities so the two packages catch boundary mistakes in both directions. - Treat any value passed as a prop to a Client Component as public — never pass raw secrets, tokens, or full database rows; pass only the sanitized fields the UI needs.
Next.js Server Components
(continued)Caching Server Output
- Server Components re-run on every request by default — without caching, a page with five database calls makes five fresh queries on every visit.
import { cacheLife, cacheTag } from "next/cache"; async function getPost(slug: string) { "use cache"; // marks this function's result as cacheable cacheTag(`post-${slug}`); cacheLife("hours"); return db.post.findUnique({ where: { slug } }); } - The
use cachedirective is the current recommended way to cache fetches, components, or whole routes, replacing the olderunstable_cacheAPI for new code. - Revalidate a specific cache entry on demand with
revalidateTag("post-my-slug")after a mutation, instead of waiting forcacheLifeto expire. fetchcalls still support the older{ next: { revalidate: 3600 } }option directly, which works without enabling theuse cachedirective.const res = await fetch("https://api.example.com/data", { next: { revalidate: 3600 } });- Wrap slow server queries in
<Suspense>so the rest of the page streams in while one section is still loading.<Suspense fallback={<p>Loading…</p>}> <SlowServerSection /> </Suspense>
Next.js Server Components
(FAQ)FAQ
Yes — server components run in a Node.js environment and can call your database, ORM, or internal services directly using async/await. You only need an API route when the client itself needs to fetch data after the initial page load.
Add "use client" as the very first line of the file to make it a client component. Only components that actually need browser APIs, event listeners, or React hooks need this directive — keep it scoped to the smallest component possible.
Pass the data as props from the server component down to the client component — but only serialize-safe values like strings, numbers, and plain objects. Functions, class instances, and Promises cannot cross the server-client boundary as props.
Any module imported by a client component (directly or transitively) gets bundled for the browser. Use the server-only package in files that must stay server-side — it throws a build-time error if that module is ever pulled into the client graph.
You can't import a server component inside a client component, but you can pass one as children or a prop — the server component still renders on the server, and only its output HTML is sent to the client component as a slot. This is the key pattern for keeping heavy server logic out of the client bundle.
Next.js Testing
Test Next.js apps with Jest, React Testing Library, and E2E testing with Playwright or Cypress.
TL;DR
- 01Use Jest for unit and integration tests of functions and components.
- 02Use React Testing Library to test components from the user perspective.
- 03Use Playwright or Cypress for end-to-end testing full user workflows.
Tips
- 01Aim for a test pyramid: many unit tests, some integration tests, and a few critical E2E tests to balance speed and confidence.
- 02Use Mock Service Worker (MSW) to intercept network requests in integration-style tests, since it mimics real API behavior more closely than manual <code>jest.mock</code> calls.
Warnings
- 01Avoid testing implementation details and focus on user behavior, since refactored code will break tests that depend on internal structure.
- 02E2E tests that wait on fixed timeouts instead of specific conditions or selectors become flaky, since real network and render timing varies between runs.
Next.js Testing
(continued)Setup and Jest Basics
- Next.js doesn't ship a test runner out of the box — install Jest and configure it with the built-in Next.js preset to get started.
npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom - Create
jest.config.jsusing the built-in Next.js Jest preset.const nextJest = require("next/jest"); const createJestConfig = nextJest({ dir: "./" }); module.exports = createJestConfig({ testEnvironment: "jsdom" }); - Create test files with
.test.jsor.spec.jsextensions.// utils.test.js import { add } from "./utils"; it("adds two numbers", () => { expect(add(2, 3)).toBe(5); }); - Run tests in watch mode to re-run on file changes.
npm test -- --watch - Use
describeto group related tests anditfor individual test cases. Jest runs tests in parallel by default.
Next.js Testing
(continued)Testing Components
- Tests that check internal state break on every refactor — import React Testing Library to test components the way a user actually sees them.
import { render, screen } from "@testing-library/react"; import Button from "./Button"; it("renders a button", () => { render(<Button>Click me</Button>); const btn = screen.getByRole("button", { name: /click me/i }); expect(btn).toBeInTheDocument(); }); - Query elements using semantic methods like
getByRole,getByText.screen.getByRole("button", { name: /submit/i }); screen.getByLabelText("Email"); screen.getByText("Welcome"); - Test user interactions with
userEventinstead offireEvent.import userEvent from "@testing-library/user-event"; const user = userEvent.setup(); await user.click(button); - Avoid testing implementation details, focus on what users see and do.
Next.js Testing
(continued)Testing API Routes
- No server needed to test an API route — create separate test files that call route handlers directly with mocked request and response objects.
import { createMocks } from "node-mocks-http"; import handler from "./api/hello"; it("returns a greeting", async () => { const { req, res } = createMocks({ method: "GET" }); await handler(req, res); expect(res._getStatusCode()).toBe(200); expect(JSON.parse(res._getData())).toHaveProperty("message"); }); - Use a library like
node-mocks-httpto mock request and response. - Test different HTTP methods and status codes.
- Mock dependencies like databases or external APIs.
Next.js Testing
(continued)Mocking and Fixtures
- A flaky network call shouldn't fail your test suite — mock external API calls to keep tests fast and isolated.
jest.mock("./api", () => ({ fetchUser: jest.fn(() => Promise.resolve({ id: 1, name: "Alice" })) })); - Use fixtures to provide consistent test data.
const mockUser = { id: 1, name: "Alice", email: "alice@example.com" }; it("displays user info", () => { render(<Profile user={mockUser} />); expect(screen.getByText("Alice")).toBeInTheDocument(); }); - Mock the App Router navigation module for components using
useRouterorusePathname.jest.mock("next/navigation", () => ({ useRouter: jest.fn(() => ({ push: jest.fn(), replace: jest.fn() })), usePathname: jest.fn(() => "/"), useSearchParams: jest.fn(() => new URLSearchParams()) })); - Mocking prevents external dependencies from slowing tests.
Next.js Testing
(continued)E2E Testing
- Unit tests can't catch a broken checkout flow across pages — use Playwright or Cypress for end-to-end testing of full workflows.
// playwright.spec.ts import { test, expect } from "@playwright/test"; test("user can sign up", async ({ page }) => { await page.goto("http://localhost:3000"); await page.fill("input[name=email]", "test@example.com"); await page.click("button:has-text('Sign Up')"); await expect(page).toHaveURL("/dashboard"); }); - E2E tests run in a real browser and test the entire application.
- Run E2E tests against a deployed environment before releasing.
- E2E tests are slower but catch integration issues that unit tests miss.
npx playwright test - Use E2E tests for critical user paths like checkout or login.
Next.js Testing
(FAQ)FAQ
Install jest, jest-environment-jsdom, @testing-library/react, and @testing-library/jest-dom, then add a jest.config.js using Next.js's built-in preset: const nextJest = require('next/jest'); module.exports = nextJest({ dir: './' })({testEnvironment: 'jsdom'}).
Both work well, but Playwright is generally preferred for new projects due to faster execution, built-in multi-browser support, and better handling of Next.js server components. Cypress has a more mature ecosystem and better real-time debugging.
Import the handler function directly and call it with mocked req/res objects using libraries like node-mocks-http, then assert on res.status() and res.json() values without spinning up a server.
This usually means the element is conditionally rendered or loaded asynchronously—use async queries like findByText or waitFor instead of getByText so the test waits for the DOM to update after data fetching or state changes.
Mock the next/navigation module (App Router) or next/router (Pages Router) using jest.mock: jest.mock('next/navigation', () => ({ useRouter: () => ({ push: jest.fn(), pathname: '/' }) })) at the top of your test file.
Next.js Analytics and Monitoring
Integrate analytics, error tracking, and performance monitoring into your Next.js app.
TL;DR
- 01Use web-vitals library to measure Core Web Vitals automatically.
- 02Integrate Sentry to capture and track errors in production.
- 03Add Google Analytics to monitor user behavior and page views.
Tips
- 01Use web-vitals and Sentry together for complete visibility into user experience and error rates.
Warnings
- 01Be mindful of privacy when tracking user data — follow GDPR and CCPA regulations when storing user information.
Next.js Analytics and Monitoring
(continued)Web Vitals
- Use
useReportWebVitalsfromnext/vitalsto capture Core Web Vitals in App Router."use client"; import { useReportWebVitals } from "next/vitals"; export function WebVitals() { useReportWebVitals((metric) => { console.log(metric); // { name, value, id, ... } }); return null; } // Place <WebVitals /> in app/layout.tsx - Core Web Vitals: CLS, INP (replaced FID), LCP, FCP, TTFB.
- Essential metrics for user experience and Google SEO ranking.
- Send vitals to a custom analytics endpoint.
import { getCLS, getINP, getLCP } from "web-vitals"; function sendToAnalytics({ name, value, id }) { fetch("/api/vitals", { method: "POST", body: JSON.stringify({ name, value, id }) }); } getCLS(sendToAnalytics); getINP(sendToAnalytics); getLCP(sendToAnalytics); - For Pages Router, export
reportWebVitalsfrompages/_app.js.// pages/_app.js — Pages Router only export function reportWebVitals(metric) { console.log(metric); // { name, value, id, startTime, ... } }
Next.js Analytics and Monitoring
(continued)Error Tracking with Sentry
- Integrate Sentry for error monitoring.
npm install @sentry/nextjs - Set up Sentry in your Next.js config.
// next.config.js const { withSentryConfig } = require("@sentry/nextjs"); module.exports = withSentryConfig({ // your Next.js config }, { org: "your-org", project: "your-project", authToken: process.env.SENTRY_AUTH_TOKEN }); - Capture errors automatically on client and server.
import * as Sentry from "@sentry/nextjs"; Sentry.captureException(error); - Capture exceptions inside error boundaries manually.
// app/error.tsx "use client"; import * as Sentry from "@sentry/nextjs"; import { useEffect } from "react"; export default function Error({ error, reset }) { useEffect(() => { Sentry.captureException(error); }, [error]); return <button onClick={reset}>Try again</button>; } - Add user context to Sentry for better debugging.
Sentry.setUser({ id: user.id, email: user.email });
Next.js Analytics and Monitoring
(continued)Google Analytics
- Add Google Analytics for user tracking.
// app/layout.tsx import Script from "next/script"; export default function RootLayout({ children }) { return ( <html> <head> <Script src="https://www.googletagmanager.com/gtag/js?id=GA_ID" strategy="afterInteractive" /></Script> </head> <body>{children}</body> </html> ); } - Track page views and custom events.
function trackEvent(name: string) { window.gtag('event', name); } - Track page views on route changes in App Router.
"use client"; import { usePathname } from "next/navigation"; import { useEffect } from "react"; export function Analytics() { const pathname = usePathname(); useEffect(() => { window.gtag?.("event", "page_view", { page_path: pathname }); }, [pathname]); return null; } - Track ecommerce events for purchases and conversions.
function trackPurchase(order) { window.gtag("event", "purchase", { transaction_id: order.id, value: order.total, currency: "USD" }); } - Use Google Tag Manager for managing multiple scripts.
<Script src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXX" strategy="afterInteractive" />
Next.js Analytics and Monitoring
(continued)Custom Metrics
- Measure custom metrics with events.
// Track API response times export async function POST(request: Request) { const start = performance.now(); try { const result = await processRequest(request); const duration = performance.now() - start; // Log metric console.log(`Request took ${duration}ms`); return Response.json(result); } catch (error) { Sentry.captureException(error); return Response.json({ error: "Failed" }, { status: 500 }); } } - Send metrics to monitoring service.
async function recordMetric(name: string, value: number) { await fetch("/api/metrics", { method: "POST", body: JSON.stringify({ name, value }) }); } - Store custom metrics in a database via API route.
// app/api/metrics/route.ts export async function POST(request: Request) { const { name, value } = await request.json(); await db.metric.create({ data: { name, value, at: new Date() } }); return Response.json({ ok: true }); } - Track user interactions like button clicks or searches.
function trackSearch(query: string) { fetch("/api/metrics", { method: "POST", body: JSON.stringify({ name: "search", value: query.length }) }); } - Use the Performance API to measure render times.
performance.mark("component-start"); // ... render performance.mark("component-end"); performance.measure("component", "component-start", "component-end");
Next.js Analytics and Monitoring
(continued)Performance Monitoring
- Use Lighthouse CI to monitor performance.
npm install -g @lhci/cli@* lhci autorun - Monitor bundle size with package.json scripts.
{ "scripts": { "analyze": "ANALYZE=true npm run build" } } - Set up performance budgets.
// next.config.js const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: process.env.ANALYZE === "true" }); - Run Lighthouse CI in a GitHub Actions workflow.
- name: Run Lighthouse CI run: lhci autorun env: LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_TOKEN }} - Set performance score thresholds to fail CI on regressions.
{ "assert": { "assertions": { "categories:performance": ["error", { "minScore": 0.9 }] } } }
Next.js Analytics and Monitoring
(FAQ)FAQ
In the App Router, import useReportWebVitals from next/vitals and call it inside a Client Component placed in your root layout — Next.js passes each metric (CLS, INP, LCP, FCP, TTFB) to your callback. For the Pages Router, export a reportWebVitals function from pages/_app.js instead.
Install @sentry/nextjs and run npx @sentry/wizard@latest -i nextjs to auto-configure sentry.client.config.js, sentry.server.config.js, and sentry.edge.config.js. The wizard also wraps your next.config.js with withSentryConfig to enable source map uploads for readable stack traces.
Yes — create a client component that loads the gtag script via next/script with strategy='afterInteractive' and place it in your root layout. Call gtag('event', ...) for custom events, and use usePathname with a useEffect to fire page view events on route changes.
reportWebVitals captures browser-measured paint and layout metrics (CLS, LCP, etc.) that reflect perceived page speed, while Sentry performance monitoring traces request durations, API calls, and server-side spans across the full stack. Use both together to correlate slow backend operations with poor front-end user experience scores.
Use the performance.mark() and performance.measure() Web APIs in client components to instrument specific interactions, then read results with performance.getEntriesByType('measure') and send them to your own /api/metrics endpoint. For server-side timing, use Date.now() around data-fetching logic inside Server Components or Route Handlers and log to your observability backend.
Next.js Performance Optimization
Profile apps, optimize bundle size, and implement advanced caching and streaming.
TL;DR
- 01Use dynamic imports to split code and reduce bundles.
- 02Enable compression and minification in production.
- 03Implement streaming for faster first contentful paint.
Tips
- 01PPR works best for pages where the majority of content is static but a small slice (cart count, user greeting, recommendations) is dynamic. Pages that are fully dynamic benefit more from standard SSR.
Warnings
- 01Never use a <link rel="preconnect"> to Google Fonts alongside next/font/google — next/font downloads and self-hosts the font at build time so the Google CDN is never hit in production.
Next.js Performance Optimization
(continued)Bundle Size Analysis
- Check bundle size with
@next/bundle-analyzer.npm install -D @next/bundle-analyzer - Configure
next.config.jsto analyze bundles.const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: process.env.ANALYZE === "true", }); module.exports = withBundleAnalyzer({}); - Run analysis to find large dependencies.
ANALYZE=true npm run build - Look for duplicate or unused dependencies to remove.
- Use
ANALYZE=true npm run buildto open an interactive bundle treemap in the browser. - Confirm bundle wins actually improve real load times by measuring Core Web Vitals (LCP, INP, CLS) with the
web-vitalslibrary or a Lighthouse run, not bundle size alone.npx lighthouse https://your-app.com --view
Next.js Performance Optimization
(continued)Code Splitting Strategies
- Use dynamic imports to split code at route level.
import dynamic from "next/dynamic"; const HeavyComponent = dynamic(() => import("./HeavyComponent")); export default function Page() { return <HeavyComponent />; } - Split large components loaded conditionally.
const Editor = dynamic(() => import("./Editor"), { loading: () => <p>Loading editor...</p>, }); - Lazy load images with the Image component.
import Image from "next/image"; <Image src="/hero.jpg" alt="Hero" loading="lazy" width={1200} height={600} /> - Optimize barrel imports from large libraries with
optimizePackageImportsnested under the top-levelexperimentalkey innext.config.js— it is still an experimental flag, so the wrapper is required for it to take effect.// next.config.js module.exports = { experimental: { optimizePackageImports: ["lodash", "date-fns"], }, }; - Use barrel file tree-shaking to avoid importing entire libraries.
// Bad: imports entire lodash import _ from "lodash"; // Good: imports only what you need import debounce from "lodash/debounce"; - Measure whether your splitting actually helped by reporting Core Web Vitals (LCP, CLS, INP) from the client with
useReportWebVitalsfromnext/web-vitals, or by enabling Vercel Speed Insights for real-user production data.// app/_components/web-vitals.tsx "use client"; import { useReportWebVitals } from "next/web-vitals"; export function WebVitals() { useReportWebVitals((metric) => { console.log(metric); // send to your analytics endpoint instead }); return null; }
Next.js Performance Optimization
(continued)Partial Prerendering
Enable Partial Prerendering (PPR) in Next.js 15 to serve a static shell instantly while streaming dynamic slots.
// next.config.js module.exports = { experimental: { ppr: true } };Wrap only the dynamic parts of a page in
<Suspense>— the static shell is served from the CDN edge, dynamic content streams in.// app/product/[id]/page.tsx import { Suspense } from "react"; import { ProductDetails } from "./ProductDetails"; // static — pre-rendered import { RecommendedProducts } from "./RecommendedProducts"; // dynamic export default function ProductPage() { return ( <div> <ProductDetails /> {/* served immediately from CDN */} <Suspense fallback={<p>Loading recommendations...</p>}> <RecommendedProducts /> {/* streamed after static shell */} </Suspense> </div> ); }Mark a component as dynamic using
connection()fromnext/server— this opts it out of static prerendering.import { connection } from "next/server"; export async function Cart() { await connection(); // forces this component to be dynamic const cart = await fetchCart(); return <ul>{cart.map(item => <li key={item.id}>{item.name}</li>)}</ul>; }PPR is composable — you can have multiple
<Suspense>slots per page, each streaming independently.PPR reduces Time To First Byte (TTFB) for pages with mixed static and dynamic content compared to full SSR.
Next.js Performance Optimization
(continued)Compression and Minification
- Enable Gzip compression for responses.
// next.config.js module.exports = { compress: true, // enabled by default }; - Use React strict mode to catch performance issues.
// next.config.js module.exports = { reactStrictMode: true, }; - Minify HTML, CSS, and JavaScript automatically.
npm run build - Check production bundle size after optimization.
- Remove console statements in production with compiler options.
// next.config.js module.exports = { compiler: { removeConsole: { exclude: ["error"] } } };
Next.js Performance Optimization
(continued)Image and Font Optimization
Use the
<Image>component for automatic format conversion, resizing, and lazy loading.import Image from "next/image"; <Image src="/hero.jpg" alt="Hero banner" width={1200} height={600} priority // preload above-the-fold images; omit for below-fold quality={80} // default is 75; lower for smaller files sizes="(max-width: 768px) 100vw, 50vw" // tell browser which size to download />Set
priorityon hero images to preload them — only use it for images visible without scrolling.Use the
sizesprop so the browser downloads the smallest image that fits the layout.// Full-width on mobile, half-width on desktop <Image src="/photo.jpg" alt="Photo" fill sizes="(max-width: 640px) 100vw, 50vw" />Load Google Fonts with zero layout shift using
next/font/google.import { Inter } from "next/font/google"; const inter = Inter({ subsets: ["latin"], display: "swap", // avoids invisible text during font load variable: "--font-inter" }); export default function RootLayout({ children }) { return <html className={inter.variable}><body>{children}</body></html>; }Self-host a local font with
next/font/localto eliminate the external Google Fonts network request.import localFont from "next/font/local"; const myFont = localFont({ src: "./fonts/GeistVF.woff2", display: "swap" });
Next.js Performance Optimization
(FAQ)FAQ
Run ANALYZE=true next build with @next/bundle-analyzer configured in next.config.js to get an interactive treemap of your client and server bundles. Look for large node_modules being included in client chunks — these are usually the easiest wins.
Use next/dynamic for components that aren't needed on initial render — modals, charts, below-the-fold sections, and anything heavy like rich text editors. Static imports are fine for everything rendered in the initial viewport, since Next.js already tree-shakes and splits by route automatically.
SSG generates pages at build time (no revalidation), ISR adds revalidate to regenerate pages in the background after a set interval, and SSR runs on every request with no built-in cache unless you add Cache-Control headers manually. For most content pages, ISR with a short revalidation window gives the best balance of freshness and performance.
Yes, Next.js enables gzip compression by default when running next start, but it's intentionally disabled when you deploy behind a reverse proxy like Nginx or a CDN — those should handle compression instead to avoid double-encoding. If you're on a platform like Vercel, Brotli compression is applied automatically at the edge.
Wrap slow data-fetching components in <Suspense> with a fallback — Next.js will stream the shell HTML immediately and flush each suspended section as it resolves. For page-level streaming, loading.tsx files in the App Router act as automatic Suspense boundaries for the entire route segment.
Next.js Streaming and Progressive Rendering
Stream content progressively with Suspense to improve perceived performance and UX.
TL;DR
- 01Use Suspense to stream content as it's ready.
- 02Show loading states while slow components render.
- 03Stream improves perceived performance and Core Web Vitals.
Tips
- 01Use streaming to show partial content quickly — users perceive the page as faster even if all data isn't ready yet.
Warnings
- 01Don't create too many Suspense boundaries — keep them at logical boundaries to avoid confusing loading states.
Next.js Streaming and Progressive Rendering
(continued)Suspense Boundaries
- Use Suspense to show loading states while data loads.
import { Suspense } from "react"; import SlowComponent from "./SlowComponent"; export default function Page() { return ( <Suspense fallback={<p>Loading...</p>}> <SlowComponent /> </Suspense> ); } - Content is streamed as soon as it's ready.
- Users see the page progressively instead of waiting.
- Use a
loading.tsxfile as a route-level Suspense boundary.// app/dashboard/loading.tsx export default function Loading() { return <div className="skeleton">Loading dashboard...</div>; } - Suspense boundaries catch async server components automatically in App Router.
// Any async server component inside Suspense is streamed async function SlowData() { const data = await fetchSlowAPI(); // streamed when ready return <ul>{data.map(d => <li key={d.id}>{d.name}</li>)}</ul>; }
Next.js Streaming and Progressive Rendering
(continued)Nested Suspense Boundaries
- Create multiple boundaries for granular control.
export default function Dashboard() { return ( <div> <Header /> <Suspense fallback={<p>Loading posts...</p>}> <Posts /> </Suspense> <Suspense fallback={<p>Loading sidebar...</p>}> <Sidebar /> </Suspense> </div> ); } - Each boundary loads independently.
- Fast components render immediately.
- Slow components show loading states separately.
- Nest boundaries to control exactly which sections block each other.
<Suspense fallback={<HeaderSkeleton />}> <Header /> <Suspense fallback={<FeedSkeleton />}> <Feed /> </Suspense> </Suspense> - Outer boundary shows while inner boundaries resolve independently.
Next.js Streaming and Progressive Rendering
(continued)Server Components with Streaming
- Server components naturally stream their data.
// app/page.tsx import { Suspense } from "react"; import Header from "./components/Header"; import Post from "./components/Post"; export default function Home() { return ( <> <Header /> <Suspense fallback={<p>Loading posts...</p>}> <Post /> </Suspense> </> ); } // components/Post.tsx (server component) async function Post() { const posts = await fetch("https://api/posts").then(r => r.json()); return posts.map(post => <article key={post.id}>{post.title}</article>); } - Server components fetch data directly.
- Data streams as soon as it's ready.
- Parallel data fetching inside server components speeds up streaming.
async function Page() { // Both fetches start at the same time const [user, posts] = await Promise.all([ fetchUser(), fetchPosts() ]); return <Profile user={user} posts={posts} />; } - Avoid waterfalls by fetching all needed data in parallel.
Next.js Streaming and Progressive Rendering
(continued)Error Boundaries While Streaming
- Wrap a streamed Suspense boundary with the nearest
error.tsxso a thrown component swaps its fallback for an error UI instead of crashing the page.// app/dashboard/error.tsx — catches errors thrown by streamed children "use client"; export default function Error({ error, reset }: { error: Error; reset: () => void }) { return ( <div> <p>Failed to load this section.</p> <button onClick={reset}>Retry</button> </div> ); } - A component that throws inside a
<Suspense>boundary already streamed to the client gets replaced in place by the nearest error boundary's UI.async function Reviews({ id }) { const reviews = await fetchReviews(id); // throws on network failure return <ul>{reviews.map(r => <li key={r.id}>{r.text}</li>)}</ul>; } // If Reviews throws after streaming starts, error.tsx output // replaces only the Reviews chunk -- sibling boundaries stay intact. - Only the failed boundary's fallback is replaced; sibling Suspense boundaries that already streamed in keep their resolved content.
- A thrown error after the initial shell has flushed swaps in via an inline script, so streamed error recovery silently no-ops if JavaScript is disabled.
- Use
try/catchinside an async Server Component to return a fallback value instead of throwing, when inline UI is better than delegating toerror.tsx.async function Reviews({ id }) { try { const reviews = await fetchReviews(id); return <ReviewList reviews={reviews} />; } catch { return <p>Reviews are unavailable right now.</p>; // no error boundary triggered } } - Place
error.tsxat the same route segment as the Suspense boundary it should protect — a parent segment's error.tsx also catches throws from streamed children further down.
Next.js Streaming and Progressive Rendering
(continued)Prioritizing Stream Order
- Load critical content first, then push secondary content behind its own boundary so it streams in later.
export default function Product({ id }) { return ( <div> <Suspense fallback={<p>Loading product...</p>}> <BasicProduct id={id} /> </Suspense> <Suspense fallback={<p>Loading reviews...</p>}> <Reviews id={id} /> </Suspense> <Suspense fallback={<p>Loading recommendations...</p>}> <Recommendations id={id} /> </Suspense> </div> ); } - Render above-the-fold content synchronously, outside any Suspense boundary, so it never waits on a fallback.
<main> <HeroSection /> {/* renders synchronously, no Suspense */} <Suspense fallback={<Skeleton />}> <BelowFoldContent /> {/* streams in after hero */} </Suspense> </main> - React flushes resolved boundaries in the order they finish, not the order they appear in JSX, so a slow first boundary never blocks a fast later one.
- Race a slow data source against a timeout and fall back to cached or partial data, so one flaky dependency can't stall its entire boundary.
async function withTimeout(promise, ms, fallback) { const timeout = new Promise(resolve => setTimeout(() => resolve(fallback), ms)); return Promise.race([promise, timeout]); } - Deferred, lower-priority sections streaming in after the hero improve Largest Contentful Paint without delaying it.
Next.js Streaming and Progressive Rendering
(continued)State Transition Updates
- Keep UI responsive during state changes.
import { useTransition } from "react"; export default function Page({ id }) { const router = useRouter(); const [isPending, startTransition] = useTransition(); function handleNavigate() { startTransition(() => { router.push(`/product/${id}`); }); } return ( <> {isPending && <p>Loading...</p>} <button onClick={handleNavigate}> View Product </button> </> ); } - useTransition keeps UI responsive during async operations.
- Show loading state without blocking interaction.
- Mark expensive state updates as non-urgent with startTransition.
const [isPending, startTransition] = useTransition(); function handleFilter(value) { setInputValue(value); // urgent: update input immediately startTransition(() => { setFilteredList(filterItems(value)); // non-urgent: can wait }); } - useDeferredValue defers a value update to keep UI responsive.
const deferredQuery = useDeferredValue(query); // deferredQuery lags behind query, preventing expensive re-renders return <Results query={deferredQuery} />;
Next.js Streaming and Progressive Rendering
(FAQ)FAQ
Streaming sends the initial HTML shell immediately, then flushes chunks as server components resolve, which improves Time to First Byte (TTFB) and Largest Contentful Paint (LCP) since the browser can start rendering and hydrating before all data is fetched.
Yes — nested Suspense boundaries let inner components show their own fallback independently of outer ones, so a fast sidebar can render while a slow feed still shows a skeleton. Each boundary resolves and hydrates as soon as its own async work completes.
loading.js creates an implicit Suspense boundary around an entire route segment and applies automatically to all navigations to that route, while explicit
Move the slow fetch inside its own async Server Component, then wrap that component in <Suspense fallback={
This happens when the async operation resolves before React flushes the boundary, but the fallback is still briefly painted. Wrap the component with startTransition on the client side for navigation updates, or use the new React 19 use() hook with a cached promise to avoid unnecessary fallback renders on re-renders.