Next.js App Router Middleware Caching for Fast Fresh Data
Maksudur Rahman
Developers often struggle to cache API responses in Next.js App Router middleware without serving stale data. This guide shows how to implement edge‑middleware caching with stale‑while‑revalidate, keeping latency low while guaranteeing fresh content.
TL;DR / Quick Summary
Prerequisites: Next.js 14, TypeScript 5.5, Node.js 20, Vercel account.
Add a
proxy.ts(ormiddleware.ts) at the app root.Use the Edge Cache API with
Cache-Control: stale-while-revalidate.Invalidate via cache‑tags or manual
caches.default.delete.Deploy as a Vercel Edge Function for sub‑second response times.
Prerequisites & Environment
Tool | Version |
|---|---|
Node.js | 20.x |
Next.js | 14.x |
TypeScript | 5.5 |
Vercel CLI (optional) | latest |
Make sure your project is initialized with the App Router (app/directory) and the experimental.edge flag is enabled (default in Next.js 14).
1. Create Edge Middleware (proxy.ts)
Create the file/app/proxy.ts (the new name for the deprecated middleware.js). This file runs on the Edge network before the request reaches your route handlers.
// /app/proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export const config = {
// Apply to all API routes under /api/*
matcher: ["/api/:path*"],
};
export async function middleware(request: NextRequest) {
const url = request.nextUrl.clone();
// Build a cache‑key that mirrors the incoming request
const cacheKey = new Request(url.toString(), {
method: request.method,
headers: request.headers,
});
// 1️⃣ Try to serve a cached response
const cached = await caches.default.match(cacheKey);
if (cached) {
// Kick off a background revalidation so the next request gets fresh data
revalidate(cacheKey);
return cached;
}
// 2️⃣ No cache – forward to the origin (your API route)
const originResp = await fetch(url.toString(), {
method: request.method,
headers: request.headers,
// `revalidate: 0` tells Next.js not to apply its own ISR logic here
next: { revalidate: 0 },
});
// 3️⃣ Attach a SWR‑compatible Cache‑Control header
const cacheControl = "public, max-age=0, stale-while-revalidate=30"; // 30 s SWR window
const resp = new NextResponse(originResp.body, {
status: originResp.status,
headers: {
...Object.fromEntries(originResp.headers.entries()),
"Cache-Control": cacheControl,
},
});
// 4️⃣ Store the fresh response for the next request
await caches.default.put(cacheKey, resp.clone());
return resp;
}
// Background revalidation – fire‑and‑forget
async function revalidate(request: Request) {
try {
const fresh = await fetch(request);
await caches.default.put(request, fresh.clone());
} catch {
// Silently ignore – stale content will still be served
}
}How it works
Edge cache is checked first. If a hit occurs, the response is returned immediately.
A background
revalidatecall refreshes the cache without delaying the client.The
Cache-Controlheader tells downstream CDNs (Vercel Edge) to keep the stale copy for 30 seconds while a fresh copy is fetched.
2. Handling Dynamic Routes
For routes like/api/users/[id], you need a cache key that includes the dynamic segment.
// /app/proxy.ts (excerpt for dynamic routes)
export const config = {
matcher: ["/api/users/:id*"],
};
// Inside middleware – the URL already contains the id, so the same `cacheKey`
// logic works out‑of‑the‑box. If you want per‑user isolation, add a custom header:
const cacheKey = new Request(url.toString(), {
method: request.method,
headers: new Headers({
"x-user-id": url.searchParams.get("id") ?? "unknown",
}),
});3. Cache‑Tag Invalidation Strategy
When data changes (e.g., a product update), you can purge related Edge cache entries using cache tags. Next.js does not expose a built‑in tag API, but you can emulate it with a small helper.
// /lib/cacheTag.ts
export async function purgeTag(tag: string) {
// Vercel Edge does not support tag listing, so we store a map in KV (or Redis)
const keys = await getKeysForTag(tag); // implement your own store
for (const key of keys) {
await caches.default.delete(new Request(key));
}
}
// Usage in an API route after a successful mutation
import { purgeTag } from "@/lib/cacheTag";
export async function POST(req: Request) {
// ...update DB...
await purgeTag("products-list");
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}In the middleware, attach the tag to the cache entry:
await caches.default.put(cacheKey, resp.clone(), {
// @ts-ignore – Edge API accepts a third options arg for tags (future‑proof)
tags: ["products-list"],
});4. Deploying to Vercel Edge
Run
vercel loginandvercelin your project root.Ensure
output: "standalone"is not set – Edge Functions require the default serverless mode.In
vercel.json, optionally set:
{
"functions": {
"app/proxy.ts": {
"runtime": "edge"
}
}
}Deploy with
vercel --prod. The middleware will be served from the Edge network, giving sub‑millisecond latency.
5. Comparison of Cache‑Control Strategies
Use‑case | Cache‑Control Header | Revalidate Window |
|---|---|---|
Static catalog data |
| 24 h cache, 5 min SWR |
Frequently changing news |
| 0 s cache, 30 s SWR |
Private user profile |
| Never cached |
Common Pitfalls & Troubleshooting
Symptom | Likely Cause | Fix |
|---|---|---|
Middleware never runs |
| Verify |
Stale data served forever | Missing | Ensure |
500 error on revalidation |
| Forward required auth headers when calling |
Vercel deploy shows “Edge Function not supported” | Using | Remove |
FAQ
How do I limit the size of cached responses? Set a Cache-Control: max‑size=…directive (supported by Vercel Edge) or manually check response.body size before caches.default.put.
Can I cache POST requests? Edge cache only stores GET‑compatible responses. For POST, return a Location header pointing to a GET endpoint that can be cached.
What happens if the revalidation fetch fails? The background revalidate function catches errors silently, so the stale response continues to be served until the next successful fetch.
Do I need to clear the cache when I deploy a new version? Vercel automatically invalidates Edge caches on deployment, but if you use custom tags you may want to call purgeTag for critical data sets.
Related Engineering Guides
Connect with KoderSolution 🚀
Master modern software engineering, scalable architectures, and advanced backend systems.
Read more architectural deep-dives, systems tutorials, and engineering guides on kodersolution.com.
Subscribe to @kodersolution for concise, animated system design breakdowns and technical shorts.
Follow our official page for daily development tips, architecture discussions, and community updates.
Recommended Resources & Courses
Maksudur Rahman
Senior Developer Advocate & System Lead Moderator.