Caching without invalidation is just stale data. Next.js gives you precise, fast revalidation primitives.
Invalidate every cached entry that was tagged with a matching string.
1// app/actions/publish.ts
2"use server";
3import { revalidateTag } from "next/cache";
4
5export async function publishCourse(courseId: string) {
6 await db.course.update({ where: { id: courseId }, data: { status: "PUBLISHED" } });
7 revalidateTag("course"); // wipe everything tagged "course"
8 revalidateTag(`course:${courseId}`); // and this specific course
9}Invalidate the cache for a specific URL path. Useful when you don't have tags.
1import { revalidatePath } from "next/cache";
2
3export async function addReview(courseSlug: string, rating: number, comment: string) {
4 await db.review.create({ /* … */ });
5 revalidatePath(`/courses/${courseSlug}`); // single page
6 revalidatePath("/courses", "page"); // /courses listing
7 revalidatePath("/(marketing)", "layout"); // whole layout subtree
8}The second argument disambiguates whether you're invalidating a page or a layout.
A common pattern: your CMS publishes content → it hits a Next.js route handler → that handler calls revalidateTag.
1// app/api/revalidate/route.ts
2import { revalidateTag } from "next/cache";
3import { NextRequest } from "next/server";
4
5export async function POST(req: NextRequest) {
6 const secret = req.nextUrl.searchParams.get("secret");
7 if (secret !== process.env.REVALIDATE_SECRET) {
8 return new Response("Unauthorized", { status: 401 });
9 }
10
11 const { tag } = await req.json();
12 revalidateTag(tag);
13 return Response.json({ revalidated: true, tag });
14}Your CMS posts to /api/revalidate?secret=… with a body like { "tag": "course:nextjs-16" }.
1const res = await fetch("https://api.example.com/v1/posts", {
2 next: { revalidate: 600 }, // re-fetch at most every 10 minutes
3});Or for a route segment:
1// app/blog/page.tsx
2export const revalidate = 600;These still work, but "use cache" + cacheLife is the preferred path for new code.
If a page must run on every request:
1// app/dashboard/page.tsx
2export const dynamic = "force-dynamic";Reading cookies/headers or calling searchParams typically does this automatically.
For pure static output:
1export const dynamic = "force-static";Useful for marketing pages where you want to assert "this must not depend on the request".
cacheTag("course") on the list, cacheTag("course", course:${id}) on each detail page. One revalidateTag("course") updates both.dynamic = "force-dynamic".cacheLife("hours") + revalidateTag on the mutation path."days") + tag invalidation on edits.