
# H3

ocache's HTTP event shape matches [h3 v2](https://h3.dev): 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

:pm-install{name="h3 ocache"}

## Cache a route

Return a standard `Response` from the wrapped handler:

```ts
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:

```ts
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:

```ts
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](/docs/query-params), [Cookies](/docs/cookies), and [Caching HTTP Handlers](/docs/handler#headers-the-handler-cant-see) 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:

```ts
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:

```ts
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](/integrations/unstorage) 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:

```ts
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](/docs/handler) — cache keys, response eligibility, and conditional requests
- [unstorage integration](/integrations/unstorage) — persistent storage adapters
- [Incremental Static Regeneration](/docs/isr) — stale-while-revalidate patterns
