Next.jsIntermediate

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.

Ready to test yourself?

Sign in to take the quiz, track progress, and earn a certificate.

Sign in