Next.js Data & Routing: Dynamic Routes, APIs & Caching
Level up your Next.js apps with dynamic route segments, Route Handlers for REST APIs, Server Actions for mutations, fetch caching strategies, and metadata for search engines.
4 sections · ~30 min · 5-question quiz (pass ≥ 70%)
1Dynamic Routes & Route Parameters
Dynamic segments use square brackets in folder names. A file at app/products/[id]/page.tsx matches /products/1, /products/abc, etc.
// app/products/[id]/page.tsx
type Props = { params: Promise<{ id: string }> };
export default async function ProductPage({ params }: Props) {
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
if (!product) notFound();
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
</article>
);
}
Catch-all segments ([...slug]) match multiple path parts. Optional catch-all ([[...slug]]) also matches the parent path with zero segments.
generateStaticParams pre-builds pages at build time for known IDs:
export async function generateStaticParams() {
const products = await db.product.findMany({ select: { id: true } });
return products.map((p) => ({ id: p.id }));
}
In Next.js 15+, params and searchParams are Promises — always await them in async Server Components. Use notFound() from next/navigation to trigger the nearest not-found.tsx boundary.
2Route Handlers: Building API Endpoints
Route Handlers live in route.ts files and export HTTP method functions — your App Router equivalent of API routes:
// app/api/users/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const users = await db.user.findMany();
return NextResponse.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}
Dynamic API routes follow the same bracket convention:
// app/api/users/[id]/route.ts
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const user = await db.user.findUnique({ where: { id } });
if (!user) return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json(user);
}
Route Handlers run on the server only. Use them for webhooks, third-party integrations, and endpoints consumed by non-React clients. For mutations triggered from your own UI, Server Actions (next section) often provide a simpler, type-safe alternative without manually parsing JSON.
3Server Actions & Data Mutations
Server Actions are async functions that run on the server, callable from forms and Client Components:
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createTodo(formData: FormData) {
const title = formData.get("title") as string;
if (!title?.trim()) throw new Error("Title required");
await db.todo.create({ data: { title: title.trim() } });
revalidatePath("/todos");
}
Wire them to a form without any client JavaScript:
// app/todos/page.tsx
import { createTodo } from "../actions";
export default async function TodosPage() {
const todos = await db.todo.findMany();
return (
<>
<form action={createTodo}>
<input name="title" placeholder="New todo" />
<button type="submit">Add</button>
</form>
<ul>{todos.map((t) => <li key={t.id}>{t.title}</li>)}</ul>
</>
);
}
Server Actions integrate with React's form pending states via useFormStatus and useActionState. After a mutation, call revalidatePath or revalidateTag to refresh cached data. Actions are POST requests under the hood — Next.js handles serialization and CSRF protection automatically.
4Fetching, Caching & Metadata for SEO
Next.js extends fetch with caching controls. By default, fetch in Server Components is cached (static):
// Cached until manually revalidated
const res = await fetch("https://api.example.com/posts");
// Never cache — always fresh
const res = await fetch(url, { cache: "no-store" });
// Revalidate every 60 seconds (ISR-style)
const res = await fetch(url, { next: { revalidate: 60 } });
// Tag-based revalidation
const res = await fetch(url, { next: { tags: ["posts"] } });
// Later: revalidateTag("posts")
Direct database calls in Server Components are not cached by fetch — use unstable_cache or React's cache() helper to memoize within a request or across requests.
Metadata improves SEO and social sharing. Export a static object or async function from page.tsx or layout.tsx:
import type { Metadata } from "next";
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.coverUrl] },
};
}
Next.js deduplicates identical fetch calls within one render pass, so calling getPost(slug) in both generateMetadata and the page component won't double your database load.