CLI
npx create-next-app@latest my-appCreate a new Next.js appnpx create-next-app@latest my-app --typescript --tailwind --appCreate with TypeScript, Tailwind, App Routernpm run devStart development server with HMRnpm run buildBuild for productionnpm run startStart production server after buildnpm run lintRun ESLint checksANALYZE=true npm run buildBuild with bundle analyzer (requires plugin)App Router - File Conventions
app/page.tsxRoute UI - renders at "/"app/blog/page.tsxRoute UI - renders at "/blog"app/blog/[slug]/page.tsxDynamic route - "/blog/:slug"app/blog/[...slug]/page.tsxCatch-all route - "/blog/a/b/c"app/blog/[[...slug]]/page.tsxOptional catch-all - also matches "/blog"app/layout.tsxShared layout wrapping child pagesapp/loading.tsxLoading UI shown during Suspenseapp/error.tsxError boundary UI for the segmentapp/not-found.tsxShown when notFound() is calledapp/route.tsAPI route handler at the pathapp/(group)/page.tsxRoute group - "(group)" not in URLapp/_components/Button.tsxPrivate folder - excluded from routingmiddleware.tsMiddleware runs before every requestServer & Client Components
"use client"Directive - marks file as Client Component (top of file)"use server"Directive - marks file/function as Server Actionexport default async function Page() { const data = await fetch(...); }Server Component - async by default, can await directlyexport default function Counter() { const [n, setN] = useState(0); }Client Component - can use hooks and browser APIsimport "use client"; // at top of fileMake entire file a Client Component<ClientComp serverProp={await getData()} />Pass server data as props to Client ComponentchildrenPass Server Components as children to Client Componentsimport { cache } from "react"; export const getData = cache(async () => { ... })Deduplicate requests across componentsData Fetching
const data = await fetch("https://api.example.com/data")Fetch in Server Component (cached by default)fetch(url, { cache: "no-store" })Always fresh - opt out of cachingfetch(url, { next: { revalidate: 60 } })Revalidate cached data every 60 seconds (ISR)fetch(url, { next: { tags: ["posts"] } })Tag fetch for on-demand revalidationrevalidateTag("posts")On-demand revalidate by tag (in Server Action or Route Handler)revalidatePath("/blog")On-demand revalidate a specific pathimport { unstable_cache } from "next/cache"Cache arbitrary async functions (like DB queries)export const dynamic = "force-dynamic"Opt page out of static generationexport const revalidate = 3600Route segment revalidation in seconds (ISR)export async function generateStaticParams() { return posts.map(p => ({ slug: p.slug })); }Pre-generate dynamic routes at build timeRoute Handlers (API Routes)
export async function GET(request: Request) { return Response.json({ ok: true }); }GET route handlerexport async function POST(request: Request) { const body = await request.json(); }POST handler with JSON bodyexport async function GET(req, { params }) { const { id } = await params; }Dynamic route params in handlernew Response(JSON.stringify(data), { status: 201, headers: { "Content-Type": "application/json" } })Custom status and headersimport { NextResponse } from "next/server"; NextResponse.json(data, { status: 201 })Next.js helper responseNextResponse.redirect(new URL("/login", request.url))Redirect in route handler or middlewareconst url = new URL(request.url); url.searchParams.get("q")Read query string paramsrequest.headers.get("authorization")Read request headerconst cookie = request.cookies.get("token")Read cookie in handlerServer Actions
"use server"; export async function createPost(formData) { ... }Define a Server Action in a separate fileasync function createPost(formData) { "use server"; ... }Inline Server Action inside Server Component<form action={createPost}><input name="title" /><button>Save</button></form>Progressive enhancement form with Server Actionconst [state, action] = useActionState(createPost, initialState)Client hook for action state and pending statusimport { redirect } from "next/navigation"; redirect("/posts")Redirect inside Server Actionimport { revalidatePath } from "next/cache"; revalidatePath("/posts")Revalidate cache after mutationimport { cookies } from "next/headers"; (await cookies()).set("token", value)Set cookie in Server ActionRouting & Navigation
import Link from "next/link"; <Link href="/about">About</Link>Client-side navigation link<Link href="/blog/[slug]" as={`/blog/${slug}`}>Link with dynamic segmentimport { useRouter } from "next/navigation"; const router = useRouter()Programmatic navigation (Client Component)router.push("/dashboard")Navigate to routerouter.replace("/login")Navigate without adding to historyrouter.back()Navigate to previous pagerouter.refresh()Refresh current route (re-fetch server data)import { usePathname } from "next/navigation"; const pathname = usePathname()Get current pathnameimport { useSearchParams } from "next/navigation"; const q = useSearchParams().get("q")Read query string (Client Component)import { useParams } from "next/navigation"; const { slug } = useParams()Read dynamic params (Client Component)import { redirect, notFound } from "next/navigation"Server-side redirect and 404Metadata & SEO
export const metadata = { title: "My Page", description: "..." }Static metadata export from page/layoutexport async function generateMetadata({ params }) { return { title: post.title }; }Dynamic metadata functiontitle: { template: "%s | My Site", default: "My Site" }Title template in root layoutopenGraph: { title: "...", description: "...", images: ["/og.png"] }Open Graph metadatatwitter: { card: "summary_large_image", title: "..." }Twitter card metadataapp/opengraph-image.tsxDynamic OG image via file conventionimport { ImageResponse } from "next/og"Generate OG images with JSXapp/robots.ts → export default function robots() { return { rules: [...] }; }robots.txt via file conventionapp/sitemap.ts → export default function sitemap() { return [...]; }sitemap.xml via file conventionImage & Font Optimization
import Image from "next/image"Import optimized Image component<Image src="/photo.jpg" alt="Photo" width={800} height={600} />Local image - width and height required<Image src={img} alt="Photo" fill className="object-cover" />Fill parent container<Image src="https://..." alt="..." width={400} height={300} />Remote image - add hostname to next.configpriorityEagerly load - use on LCP imagesizes="(max-width: 768px) 100vw, 50vw"Responsive size hints for srcsetimport { Inter } from "next/font/google"; const inter = Inter({ subsets: ["latin"] })Import Google Font<html className={inter.className}>Apply font to entire appimport localFont from "next/font/local"; const myFont = localFont({ src: "./font.woff2" })Load local font fileMiddleware
export function middleware(request: NextRequest) { ... }Default middleware export in middleware.tsexport const config = { matcher: ["/dashboard/:path*", "/api/:path*"] }Scope middleware to specific pathsNextResponse.next()Continue to next handlerNextResponse.redirect(new URL("/login", request.url))Redirect from middlewareNextResponse.rewrite(new URL("/404", request.url))Rewrite URL (user sees original path)const response = NextResponse.next(); response.headers.set("X-Custom", "value"); return response;Modify response headersrequest.cookies.get("token")?.valueRead cookie in middlewarerequest.nextUrl.pathnameGet current pathname in middlewareEnvironment Variables
.env.localLocal overrides - never committed to git.env.development / .env.productionEnvironment-specific defaults.envShared defaults - safe to commit (no secrets)NEXT_PUBLIC_API_URL=https://api.example.comNEXT_PUBLIC_ prefix exposes variable to the browserDB_URL=postgres://...No prefix - server-only, never sent to clientprocess.env.NEXT_PUBLIC_API_URLRead public env var in client or server codeprocess.env.DB_URLRead server-only env var (undefined in client components)import { env } from "process"Access env from Node process module in Server ComponentsNEXT_PUBLIC_ vars are inlined at build timeChanging them requires a rebuild - not dynamicexport const dynamic = "force-dynamic"Required if reading server env vars that change at runtimenext.config.js
/** @type {import("next").NextConfig} */JSDoc type annotation for IntelliSenseoutput: "standalone"Self-contained build for Docker deploymentoutput: "export"Fully static export - no Node.js server neededimages: { remotePatterns: [{ hostname: "cdn.example.com" }] }Allow next/image to load from remote hostimages: { formats: ["image/avif", "image/webp"] }Enable AVIF and WebP image optimizationredirects: async () => [{ source: "/old", destination: "/new", permanent: true }]Permanent (308) redirectrewrites: async () => [{ source: "/api/:path*", destination: "https://api.example.com/:path*" }]Proxy requests to another URLheaders: async () => [{ source: "/(.*)", headers: [{ key: "X-Frame-Options", value: "DENY" }] }]Add custom response headersenv: { CUSTOM_KEY: process.env.CUSTOM_KEY }Expose server env vars to the app (use NEXT_PUBLIC_ instead for client)basePath: "/docs"Serve app under a sub-pathtrailingSlash: trueRedirect /about to /about/poweredByHeader: falseRemove X-Powered-By: Next.js response headercompress: falseDisable built-in gzip compression (use reverse proxy instead)transpilePackages: ["some-esm-package"]Transpile packages in node_modules with SWCexperimental: { typedRoutes: true }Enable statically typed Links and routes