Next.js Cache
beginnerNext.js built-in caching with unstable_cache, revalidatePath, and revalidateTag.
cachingnextjsserverlessvercel
Tested on⬢20▲16⚛19TS5.9
$ bunx sinew add caching/nextjs-cacheInteractive demo coming soon
1The Problem
Adding external caching infrastructure is often overkill:
- Redis requires additional infrastructure and cost
- Managing cache invalidation across deployments is complex
- Cold starts on serverless need fast cache warming
2The Solution
Use Next.js built-in caching for simple, zero-config caching that works out of the box on Vercel and other platforms.
3Files
lib/cache.ts
lib/cache.tsTypeScript
import { unstable_cache } from "next/cache";
import { revalidatePath, revalidateTag } from "next/cache";
// Cache a function with tags for invalidation
export function cachedQuery<T>(
fn: () => Promise<T>,
keyParts: string[],
options?: {
tags?: string[];
revalidate?: number | false;
}
) {
return unstable_cache(fn, keyParts, {
tags: options?.tags,
revalidate: options?.revalidate,
});
}
// Revalidate a specific path
export function invalidatePath(path: string, type?: "page" | "layout") {
revalidatePath(path, type);
}
// Revalidate all entries with a tag
export function invalidateTag(tag: string) {
revalidateTag(tag);
}lib/data/users.ts
lib/data/users.tsTypeScript
import { prisma } from "@/lib/db";
import { cachedQuery, invalidateTag } from "@/lib/cache";
export const getUser = (id: string) =>
cachedQuery(() => prisma.user.findUnique({ where: { id } }), ["user", id], {
tags: [`user:${id}`, "users"],
revalidate: 3600,
})();
export const getUsers = () =>
cachedQuery(() => prisma.user.findMany({ orderBy: { createdAt: "desc" } }), ["users"], {
tags: ["users"],
revalidate: 60,
})();
export async function updateUser(id: string, data: { name: string }) {
const user = await prisma.user.update({ where: { id }, data });
invalidateTag(`user:${id}`);
invalidateTag("users");
return user;
}app/users/[id]/page.tsx
app/users/[id]/page.tsxTypeScript
import { getUser } from "@/lib/data/users";
import { notFound } from "next/navigation";
export default async function UserPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const user = await getUser(id);
if (!user) {
notFound();
}
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}app/api/users/[id]/route.ts
app/api/users/[id]/route.tsTypeScript
import { NextRequest, NextResponse } from "next/server";
import { updateUser } from "@/lib/data/users";
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const data = await req.json();
const user = await updateUser(id, data);
return NextResponse.json(user);
}4Configuration
Cache Revalidation Options
// Revalidate every 60 seconds
{
revalidate: 60;
}
// Never revalidate (static)
{
revalidate: false;
}
// Don't specify - uses page/route segment config
{
}TypeScript
Route Segment Config
// app/users/page.tsx
export const revalidate = 60; // Revalidate every 60 seconds
export const dynamic = "force-static"; // Force static generationTypeScript
5Usage
On-Demand Revalidation
// In a Server Action or Route Handler
import { revalidatePath, revalidateTag } from "next/cache";
// Revalidate a specific page
revalidatePath("/users");
// Revalidate all pages using a tag
revalidateTag("users");
// Revalidate a layout (and all nested pages)
revalidatePath("/dashboard", "layout");TypeScript
Webhook Revalidation
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from "next/server";
import { revalidateTag } from "next/cache";
export async function POST(req: NextRequest) {
const secret = req.headers.get("x-revalidate-secret");
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: "Invalid secret" }, { status: 401 });
}
const { tag } = await req.json();
revalidateTag(tag);
return NextResponse.json({ revalidated: true });
}TypeScript
6Troubleshooting
Cache not invalidating
- Ensure you're calling
revalidateTagorrevalidatePathin a Server Action or Route Handler - Check that tags match exactly (they're case-sensitive)
- Remember that
unstable_cachecaches are per-deployment
Stale data in development
- Next.js caching behaves differently in dev mode
- Use
next build && next startto test production caching behavior