ETag / conditional GET
The endpoint emits an ETag header. Subsequent requests with If-None-Match: <etag> get a 304 with no body — same freshness check, near-zero bytes. Pair with cache-control: max-age=N, must-revalidate to keep the browser cache hot but always re-validate after expiry.
Tip
A strong ETag (quoted: <code>"abc123"</code>) requires byte-for-byte equality. A weak ETag (prefixed <code>W/</code>) allows semantic equivalence. Prefer strong ETags for JSON APIs: the body must be bit-identical, not just "equivalent". Derive the ETag from a hash of the body (<code>fnv1a</code>, <code>sha256</code>) rather than a timestamp to avoid spurious misses.
Try it
First click: 200 with body. Subsequent clicks: 304 (server's ETag matches the one we sent).
// src/app/api/etag-data/route.ts — conditional GET.
const ETAG = '"' + fnv1a(BODY) + '"';
export async function GET(req: Request): Promise<Response> {
if (req.headers.get("if-none-match") === ETAG) {
return new Response(null, { status: 304, headers: { ETag: ETAG } });
}
return new Response(BODY, { status: 200, headers: {
"content-type": "application/json",
ETag: ETAG,
"cache-control": "public, max-age=10, must-revalidate",
}});
}
// Client revalidates on focus — fetch + ETag means most checks are
// 304s with no body, so the cost is one round-trip with no payload.
window.addEventListener("focus", () => fetch("/api/etag-data"));