On-demand Revalidation

revalidatePath and revalidateTag

When an admin edits a product, you do not want to wait for the timer. Clear the cache right away from a Server Action or Route Handler:

'use server';

import { revalidatePath, revalidateTag } from 'next/cache';
import { prisma } from '@/lib/prisma';

export async function updatePrice(id: number, price: number) {
  await prisma.product.update({ where: { id }, data: { price } });

  revalidateTag('product-' + id);    // every fetch tagged with this
  revalidatePath('/products/' + id); // this page
  revalidatePath('/products');       // the listing
}

Note: In Next.js 16, revalidateTag(tag, 'max') takes a cache-life profile as a second argument for stale-while-revalidate behaviour, and the new updateTag(tag) (Server Actions only) expires the tag immediately so the user sees their own change.

A Webhook From a CMS

app/api/revalidate/route.ts — a headless CMS or your NestJS backend calls this after content changes:

import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';

export async function POST(req: NextRequest) {
  if (req.headers.get('x-revalidate-secret') !== process.env.REVALIDATE_SECRET) {
    return Response.json({ message: 'Invalid secret' }, { status: 401 });
  }
  const { tag } = (await req.json()) as { tag: string };
  revalidateTag(tag);
  return Response.json({ revalidated: true, tag });
}

Example

curl -X POST localhost:3000/api/revalidate -H "x-revalidate-secret: s3cr3t" -H "Content-Type: application/json" -d '{"tag":"products"}'

Example output

{"revalidated":true,"tag":"products"}

Tags in TypeScript

Example

A cache where each entry has tags, and invalidating a tag removes every entry that has it:

class TaggedCache {
  private entries = new Map<string, { value: string; tags: string[] }>();

  set(key: string, value: string, tags: string[]): void {
    this.entries.set(key, { value, tags });
  }
  get(key: string): string {
    return this.entries.get(key)?.value ?? '(miss)';
  }
  revalidateTag(tag: string): void {
    for (const [key, e] of this.entries) if (e.tags.includes(tag)) this.entries.delete(key);
  }
}

const c = new TaggedCache();
c.set('/api/products/1', 'Redmi ₹17999', ['products', 'product-1']);
c.set('/api/products/2', 'boAt ₹1299', ['products', 'product-2']);
c.set('/api/categories', 'Mobiles, Audio', ['categories']);

c.revalidateTag('product-1');
console.log(c.get('/api/products/1'), '|', c.get('/api/products/2'), '|', c.get('/api/categories'));
c.revalidateTag('products');
console.log(c.get('/api/products/2'), '|', c.get('/api/categories'));

Output

(miss) | boAt ₹1299 | Mobiles, Audio
(miss) | Mobiles, Audio