From cc6f4df70cf4632b6b176d8b3dc7d55bfff14760 Mon Sep 17 00:00:00 2001 From: raed bahri Date: Thu, 12 Mar 2026 11:48:25 +0100 Subject: [PATCH 1/2] docs(universal-cache): refresh guide defaults and revalidation --- .vitepress/config.ts | 4 + docs/middleware/third-party.md | 2 + .../middleware/third-party/universal-cache.md | 228 ++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 docs/middleware/third-party/universal-cache.md diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 85f32757d..16d371165 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -219,6 +219,10 @@ const sidebars = (): DefaultTheme.SidebarItem[] => [ text: '3rd-party Middleware', link: '/docs/middleware/third-party', }, + { + text: 'Universal Cache (3rd-party)', + link: '/docs/middleware/third-party/universal-cache', + }, ], }, { diff --git a/docs/middleware/third-party.md b/docs/middleware/third-party.md index 06ca1fd43..da489dee5 100644 --- a/docs/middleware/third-party.md +++ b/docs/middleware/third-party.md @@ -88,4 +88,6 @@ Most of this middleware leverages external libraries. - [RONIN (Database)](https://github.com/ronin-co/hono-client) - [Session](https://github.com/honojs/middleware/tree/main/packages/session) - [tsyringe](https://github.com/honojs/middleware/tree/main/packages/tsyringe) +- [Universal Cache](https://github.com/honojs/middleware/tree/main/packages/universal-cache) +- [Universal Cache Guide](/docs/middleware/third-party/universal-cache) - [User Agent based Blocker](https://github.com/honojs/middleware/tree/main/packages/ua-blocker) diff --git a/docs/middleware/third-party/universal-cache.md b/docs/middleware/third-party/universal-cache.md new file mode 100644 index 000000000..3bdd2a889 --- /dev/null +++ b/docs/middleware/third-party/universal-cache.md @@ -0,0 +1,228 @@ +# Universal Cache Middleware + +`@hono/universal-cache` is a third-party middleware package for response and function-level caching in Hono apps. + +It supports: + +- response caching with `cacheMiddleware()` +- request-scoped defaults with `cacheDefaults()` +- function result caching with `cacheFunction()` +- stale-while-revalidate (SWR) +- custom keying, serialization, and cache invalidation hooks + +## Install + +```txt +npm i @hono/universal-cache +``` + +## Import + +```ts +import { Hono } from 'hono' +import { + cacheDefaults, + cacheFunction, + cacheMiddleware, +} from '@hono/universal-cache' +``` + +## Default Behavior + +Universal Cache uses an in-memory `unstorage` driver by default, with these defaults: + +```ts +const baseDefaults = { + base: 'cache', + maxAge: 60, + staleMaxAge: 0, + swr: true, + keepPreviousOn5xx: true, +} + +const middlewareDefaults = { + ...baseDefaults, + group: 'hono/handlers', + methods: ['GET', 'HEAD'], + varies: [], +} + +const functionDefaults = { + ...baseDefaults, + group: 'hono/functions', +} +``` + +Default key strategy: + +- `cacheMiddleware()`: hash of `pathname + search`, plus hashed `varies` header values +- `cacheFunction()`: hash of serialized function arguments +- middleware name default: normalized request path (e.g. `/api/items` -> `api:items`) +- function name default: `fn.name` (fallback: `_`) +- integrity default: auto-derived hash (middleware by cache namespace, function by function source) + +## Usage + +### Cache route responses + +```ts +const app = new Hono() + +app.get( + '/api/items', + cacheMiddleware({ + maxAge: 60, + staleMaxAge: 30, + swr: true, + }), + (c) => c.json({ items: ['a', 'b'] }) +) +``` + +### Set global cache defaults + +```ts +app.use( + '*', + cacheDefaults({ + maxAge: 60, + staleMaxAge: 30, + swr: true, + }) +) +``` + +### Use storage adapters (via unstorage) + +```ts +import { createStorage } from 'unstorage' +import redisDriver from 'unstorage/drivers/redis' + +app.use( + '*', + cacheDefaults({ + storage: createStorage({ + driver: redisDriver({ url: 'redis://localhost:6379' }), + }), + maxAge: 60, + staleMaxAge: 30, + swr: true, + }) +) +``` + +### Override global defaults per route + +```ts +app.get( + '/api/slow/items', + cacheMiddleware({ + config: { maxAge: 300, staleMaxAge: 120 }, + varies: ['accept-language'], + }), + (c) => c.json({ ok: true }) +) +``` + +### Cache function results + +```ts +const getStats = cacheFunction( + async (id: string) => { + return { id, ts: Date.now() } + }, + { + maxAge: 60, + getKey: (id) => id, + } +) +``` + +## Revalidation and Invalidation + +Manual route revalidation is disabled by default. + +To allow it, set `revalidateHeader` explicitly and optionally gate it with `shouldRevalidate`: + +```ts +cacheMiddleware({ + revalidateHeader: 'x-cache-revalidate', + shouldRevalidate: (c) => c.req.header('x-admin-token') === '1', +}) +``` + +Other useful hooks: + +- `shouldBypassCache` +- `shouldInvalidateCache` +- `shouldRevalidate` + +## Options Summary + +Base options (`cacheMiddleware` + `cacheFunction`): + +- `storage`: custom unstorage instance +- `base`: storage prefix (default: `cache`) +- `group`: storage group segment (default middleware: `hono/handlers`, function: `hono/functions`) +- `name`: cache entry name (default inferred from route/function name) +- `hash`: custom hash function for default keys/integrity +- `integrity`: manual integrity value for cache busting +- `maxAge`: max age in seconds (default: `60`) +- `staleMaxAge`: stale max age in seconds, `-1` means unlimited stale (default: `0`) +- `swr`: enable stale-while-revalidate (default: `true`) +- `keepPreviousOn5xx`: keep previous entry when refresh fails in invalidation paths (default: `true`) +- `revalidateHeader`: revalidation header name (disabled by default) + +Middleware-only: + +- `config`: request-scoped defaults to apply before route options +- `methods`: cacheable methods (default: `GET`, `HEAD`) +- `varies`: request headers to include in key +- `getKey`: custom request key builder +- `serialize` / `deserialize`: custom response cache format +- `validate`: validate cached response entries +- `shouldBypassCache`: skip cache for matching requests +- `shouldInvalidateCache`: invalidate entry before refresh +- `shouldRevalidate`: allow manual revalidation for matching requests + +Function-only: + +- `getKey`: custom key builder from function args +- `serialize` / `deserialize`: custom function entry format +- `validate`: validate cached function entries +- `shouldBypassCache`: skip cache for matching calls +- `shouldInvalidateCache`: invalidate entry before refresh + +## Key APIs + +### `cacheMiddleware(options | maxAge)` + +Caches route responses (default methods: `GET`, `HEAD`). + +### `cacheDefaults(options)` + +Sets request-scoped default options for cache middleware. + +### `cacheFunction(fn, options | maxAge)` + +Wraps a function with caching behavior. + +### Storage/default helpers + +- `createCacheStorage()` +- `setCacheStorage()` / `getCacheStorage()` +- `setCacheDefaults()` / `getCacheDefaults()` + +## Notes + +- Cached route responses drop `set-cookie` and hop-by-hop headers. +- Responses with `cache-control: no-store` or `no-cache` are not cached. +- On `workerd`, stale route entries refresh synchronously instead of using background self-fetch. +- Cached function values should be serializable for your selected storage driver. + +## Links + +- Source package: + `https://github.com/honojs/middleware/tree/main/packages/universal-cache` +- Third-party middleware index: + `/docs/middleware/third-party` From 561e5afa8af3aafb4f2d1351383002fd79be7e6f Mon Sep 17 00:00:00 2001 From: raed bahri Date: Mon, 13 Jul 2026 03:22:37 +0100 Subject: [PATCH 2/2] docs: update universal cache guide --- .../middleware/third-party/universal-cache.md | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/middleware/third-party/universal-cache.md b/docs/middleware/third-party/universal-cache.md index 3bdd2a889..01a563b63 100644 --- a/docs/middleware/third-party/universal-cache.md +++ b/docs/middleware/third-party/universal-cache.md @@ -55,7 +55,8 @@ const functionDefaults = { Default key strategy: -- `cacheMiddleware()`: hash of `pathname + search`, plus hashed `varies` header values +- `cacheMiddleware()`: hash of the method, origin, path, and query; explicitly enabled non-`GET`/`HEAD` methods also include the request body +- middleware keys include configured `varies` headers and automatically vary by `authorization` and `cookie` when present - `cacheFunction()`: hash of serialized function arguments - middleware name default: normalized request path (e.g. `/api/items` -> `api:items`) - function name default: `fn.name` (fallback: `_`) @@ -140,18 +141,20 @@ const getStats = cacheFunction( ## Revalidation and Invalidation -Manual route revalidation is disabled by default. - -To allow it, set `revalidateHeader` explicitly and optionally gate it with `shouldRevalidate`: +Manual revalidation is disabled by default. Enable it with a private header name and authorize it with `shouldRevalidate`: ```ts cacheMiddleware({ - revalidateHeader: 'x-cache-revalidate', - shouldRevalidate: (c) => c.req.header('x-admin-token') === '1', + revalidateHeader: 'x-my-cache-revalidate', + shouldRevalidate: (c) => + c.req.header('authorization') === + `Bearer ${process.env.CACHE_TOKEN}`, }) ``` -Other useful hooks: +A request containing `x-my-cache-revalidate: 1` refreshes the entry only when `shouldRevalidate` allows it. Do not expose an ungated revalidation header on public endpoints. + +You can also use: - `shouldBypassCache` - `shouldInvalidateCache` @@ -171,7 +174,7 @@ Base options (`cacheMiddleware` + `cacheFunction`): - `staleMaxAge`: stale max age in seconds, `-1` means unlimited stale (default: `0`) - `swr`: enable stale-while-revalidate (default: `true`) - `keepPreviousOn5xx`: keep previous entry when refresh fails in invalidation paths (default: `true`) -- `revalidateHeader`: revalidation header name (disabled by default) +- `revalidateHeader`: private header used to request revalidation (default: disabled) Middleware-only: @@ -183,7 +186,7 @@ Middleware-only: - `validate`: validate cached response entries - `shouldBypassCache`: skip cache for matching requests - `shouldInvalidateCache`: invalidate entry before refresh -- `shouldRevalidate`: allow manual revalidation for matching requests +- `shouldRevalidate`: authorize manual revalidation requests Function-only: @@ -216,7 +219,9 @@ Wraps a function with caching behavior. ## Notes - Cached route responses drop `set-cookie` and hop-by-hop headers. -- Responses with `cache-control: no-store` or `no-cache` are not cached. +- Responses outside the 2xx range, HTTP 206 responses, and responses containing `set-cookie` are not cached. +- Responses with `cache-control: private`, `no-store`, or `no-cache`, or `Vary: *`, are not cached. +- A custom middleware `getKey` replaces the complete default key. Include every relevant tenant, authorization, cookie, method, body, and variation value. - On `workerd`, stale route entries refresh synchronously instead of using background self-fetch. - Cached function values should be serializable for your selected storage driver.