Next.js App Router Cheat Sheet
Copy production-grade code snippets for App Router routing files, Server Actions, React 19 form binding, cache revalidation, middleware, and metadata.
Page Component (app/blog/[slug]/page.tsx)
Async Server Component with async params & searchParams handling (Next.js 15 spec).
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
interface PageProps {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function BlogPostPage({ params, searchParams }: PageProps) {
// In Next.js 15+, params and searchParams are Promises that must be awaited
const { slug } = await params;
const query = await searchParams;
if (!slug) notFound();
return (
<article className="p-8">
<h1 className="text-4xl font-black">Article: {slug}</h1>
<p className="mt-2 text-gray-500">Category Filter: {query.filter || "all"}</p>
</article>
);
}Root Layout (app/layout.tsx)
Top-level shell defining <html> and <body>. Preserved across route transitions.
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "DevDossier | Developer Engineering Hub",
description: "High-performance web development, UI engineering, and technical guides.",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="dark">
<body className="antialiased bg-slate-950 text-white min-h-screen">
{children}
</body>
</html>
);
}Route Handler API (app/api/posts/route.ts)
RESTful HTTP API endpoints returning JSON with custom status codes.
// app/api/posts/route.ts
import { NextResponse, type NextRequest } from "next/server";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
return NextResponse.json(
{
success: true,
category: category || "all",
data: [{ id: "post-1", title: "Next.js 15 App Router" }],
},
{ status: 200 }
);
}
export async function POST(request: NextRequest) {
const body = await request.json();
return NextResponse.json({ created: true, body }, { status: 201 });
}Server Action Mutation & Cache Invalidation
Server-side mutation function with revalidateTag and revalidatePath.
// app/actions/updateProfile.ts
"use server";
import { revalidateTag, revalidatePath } from "next/cache";
export async function updateProfile(prevState: any, formData: FormData) {
const username = formData.get("username") as string;
if (!username || username.length < 3) {
return { error: "Username must be at least 3 characters long" };
}
// Perform database write
// await db.user.update({ where: { id }, data: { username } });
// Invalidate specific cache tags & paths
revalidateTag("user-profile", "max");
revalidatePath("/dashboard");
return { success: true, username };
}React 19 useActionState Form Binding
Bind Server Action with pending state & action result using React 19 hooks.
// components/ProfileForm.tsx
"use client";
import { useActionState } from "react";
import { updateProfile } from "@/app/actions/updateProfile";
export default function ProfileForm() {
const [state, formAction, isPending] = useActionState(updateProfile, null);
return (
<form action={formAction} className="space-y-4 max-w-md">
<input
name="username"
placeholder="Enter Username"
required
className="w-full rounded-xl border p-3 bg-slate-900 text-white"
/>
<button
type="submit"
disabled={isPending}
className="w-full rounded-xl bg-blue-600 py-3 font-bold text-white hover:bg-blue-700 disabled:opacity-50"
>
{isPending ? "Saving..." : "Update Profile"}
</button>
{state?.error && <p className="text-red-400 text-xs font-semibold">{state.error}</p>}
{state?.success && <p className="text-emerald-400 text-xs font-semibold">Updated to {state.username}</p>}
</form>
);
}Tagged Data Fetching with Revalidation
Configure fetch caching behavior and tags for revalidateTag().
// lib/getPosts.ts
export async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: {
revalidate: 3600, // Revalidate every 1 hour (ISR)
tags: ["posts-list"], // Tagged for instant on-demand purge
},
});
if (!res.ok) throw new Error("Failed to fetch posts");
return res.json();
}Edge Middleware & Protected Routes
Execute request interception before route handlers run.
// middleware.ts
import { NextResponse, type NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("session_token")?.value;
const isProtectedPath = request.nextUrl.pathname.startsWith("/dashboard");
if (isProtectedPath && !token) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/admin/:path*"],
};Dynamic OpenGraph Metadata (generateMetadata)
Generate dynamic title tags, meta descriptions, and social preview images.
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
return {
title: `${slug.replace(/-/g, " ")} | DevDossier`,
description: `In-depth reference guide for ${slug}.`,
openGraph: {
title: `${slug} Guide | DevDossier`,
images: [`/api/og?title=${encodeURIComponent(slug)}`],
},
};
}Cite & Link This Resource
Backlink MagnetWriting a blog post, GitHub README, or docs? Embed a backlink snippet or badge to cite this cheat sheet.
[Next.js 15+ App Router Cheat Sheet](https://devdossier.com/resources/nextjs-app-router-cheat-sheet) - Interactive Reference & Cheat Sheet via DevDossier