H3
ocache's HTTP event shape matches h3 v2: both expose a standard Request as event.req and a parsed URL as event.url. A cached handler can therefore be registered directly as an h3 route.
#Install
npm i h3 ocache#Cache a route
Return a standard Response from the wrapped handler:
import { H3, getRouterParam } from "h3";
import { defineCachedHandler } from "ocache";
const app = new H3();
const product = defineCachedHandler(
async (event) => {
const id = getRouterParam(event, "id");
const value = await db.products.find(id);
return Response.json(value);
},
{
name: "product",
maxAge: 60,
swr: true,
staleMaxAge: 300,
},
);
app.get("/products/:id", product);
export default app;The first request stores the response. Later requests receive the cached status, headers, and body, with generated etag, cache-control, and x-cache headers when the response does not override them.
Give handlers created in a factory or loop an explicit name. Their generated source-based names would otherwise be identical, which can make routes sharing a storage backend share entries unexpectedly.
#Return h3 values
h3 handlers may return objects, strings, and other values that h3 converts to a response. ocache converts only what a Response can carry itself, so connect h3's toResponse whenever the wrapped handler returns anything else:
import { defineHandler, getRouterParam, toResponse, type H3Event } from "h3";
import { defineCachedHandler } from "ocache";
const product = defineCachedHandler<H3Event>(
async (event) => {
const id = getRouterParam(event, "id");
return await db.products.find(id); // plain object
},
{
name: "product-json",
maxAge: 60,
toResponse: (value, event) => toResponse(value, event),
},
);
export default defineHandler(product);The conversion happens on a miss before storage. Cache hits are already standard Response objects and do not run the original h3 handler again.
Warning
The hook is not optional for a handler that returns an object. Without it, ocache throws an UnsupportedValueError for the request, because its built-in conversion cannot serialize an object and will not guess. Returning undefined — for a handler that stages its result on event.res — fails the same way.
Tip
Returning Response.json(value) directly is the smallest integration and needs no hook. Use toResponse when you want h3's normal serialization and its staged event.res status or headers.
#Request headers and query parameters
For cacheable requests, the wrapped handler can read only inputs covered by the cache key. Declare every request header and query parameter that affects its output — neither is covered by default:
const localized = defineCachedHandler(renderPage, {
name: "localized-page",
maxAge: 300,
varies: ["accept-language"],
allowQuery: ["page"],
});accept-language now reaches the h3 handler, varies the cache key, and appears in Vary. allowQuery is required for query-dependent responses: without it, the handler receives a query-less URL and no query parameter varies the key. Here, only page reaches the handler and key. See Query Parameters, Cookies, and Caching HTTP Handlers for the complete rules.
Middleware runs before the route handler and still sees the original h3 event. Put request-only data such as trace IDs in event.context if the cached handler needs it for logging; do not render unkeyed context into a cached response.
Use shouldBypassCache for authenticated or otherwise private requests that must reach the route unchanged and must never be stored:
const account = defineCachedHandler(renderAccount, {
maxAge: 60,
shouldBypassCache: (event) => event.req.headers.has("authorization"),
});#Persistent storage
Create one storage adapter and pass it to each route that should use the same backend:
import { H3 } from "h3";
import { defineCachedHandler } from "ocache";
import { cacheStorage } from "./cache-storage";
const app = new H3();
const page = defineCachedHandler(renderPage, {
name: "page",
maxAge: 60,
storage: cacheStorage,
});
app.get("/pages/:slug", page);See the unstorage integration for JSON and raw-byte adapters. Keep the storage instance at module or application scope rather than creating it inside the route handler.
#Revalidate from h3
The returned handler exposes resource-level cache methods. Pass an h3 event with the same URL and varying inputs as the resource to invalidate or expire its GET and HEAD entries:
const page = defineCachedHandler(renderPage, {
name: "page",
maxAge: 60,
swr: true,
});
app.get("/pages/:slug", page);
app.post("/admin/pages/:slug/publish", async (event) => {
const slug = getRouterParam(event, "slug");
await publishPage(slug);
const url = new URL(`/pages/${slug}`, event.url);
await page.invalidate({ req: new Request(url), url });
return { ok: true };
});Use page.expire(event) instead when the next request may receive the stale response while h3 regenerates it in the background. h3's srvx-compatible request provides waitUntil, so ocache can register storage writes and stale-while-revalidate work with supported serverless runtimes.
Note
The event passed to invalidate must resolve to the same resource URL and varying inputs as the cached route. The example constructs the public /pages/:slug URL because the admin route has a different path.
#See also
- Caching HTTP Handlers — cache keys, response eligibility, and conditional requests
- unstorage integration — persistent storage adapters
- Incremental Static Regeneration — stale-while-revalidate patterns