Cache Operations
ubean ships a route-level HTTP cache built around a swappable CacheStore. It is not a generic key/value cache client — for arbitrary application caching use ubean’s built-in storage layer (useStorage/useKV from @ubean/server).
useCacheStore()
Get or set the global cache store. Defaults to an in-memory store with LRU eviction.
import { useCacheStore, createMemoryStore } from 'ubean/server';
// Use the default in-memory store
const store = useCacheStore();
// Replace the global store (e.g. with a Redis-backed implementation)
useCacheStore(createMemoryStore(500));CacheStore interface
export interface CacheStore {
get(key: string): Promise<CacheEntry | undefined>;
set(key: string, entry: Omit<CacheEntry, 'createdAt' | 'expiresAt'>, ttl: number): Promise<void>;
delete(key: string): Promise<boolean>;
clear(): Promise<void>;
/**
* Read an entry WITHOUT updating recency or deleting expired entries.
* Optional (P9-03): used by ISR SWR to serve stale content while revalidating.
* Implementations that don't support peek should leave it undefined;
* ISR falls back to `get()` (which deletes expired entries, disabling SWR).
*/
peek?(key: string): Promise<CacheEntry | undefined>;
}Implement this interface to back the cache with Redis, KV, or any other storage. The default createMemoryStore(maxEntries = 200) evicts ~20% of the oldest entries when full. The optional peek() method (added in P9-03) is required for ISR stale-while-revalidate — it returns the raw entry even if expired, without touching LRU recency or evicting it.
createMemoryStore()
import { createMemoryStore } from 'ubean/server';
const store = createMemoryStore(1000); // up to 1000 entriescreateCacheMiddleware()
Mount cache as a Hono middleware driven by route rules. Used internally by the ubean runtime when routeRules declares cache.
import { createCacheMiddleware, useCacheStore } from 'ubean/server';
app.use(createCacheMiddleware({
store: useCacheStore(),
rules: {
'/api/products/**': { ttl: 60 }, // 60s
'/api/feed': { ttl: 300, swr: true } // 5min, stale-while-revalidate
},
defaultTtl: 0
}));| Option | Type | Description |
|---|---|---|
| store | CacheStore | Store implementation (defaults to global) |
| rules | Record<string, CacheRule> | Path pattern → cache rule mapping |
| defaultTtl | number | TTL applied when no rule matches (0 disables) |
Path patterns support * (single segment) and ** (multi-segment), matching the same semantics as routeRules.
CacheRule
export interface CacheRule {
ttl: number; // seconds; 0 disables caching
swr?: boolean | number; // only emits the stale-while-revalidate response
// header (CDN semantics); app-level stale serving
// while revalidating is ISR-only
name?: string; // explicit cache key (defaults to method + path + query)
}cachedEventHandler()
Wrap a single handler with caching. Useful for expensive endpoints that do not fit the path-pattern rule model.
import { defineHandler, cachedEventHandler } from 'ubean/server';
export const GET = defineHandler(
cachedEventHandler(
async c => {
const data = await expensiveCompute();
return c.json(data);
},
{ ttl: 300, name: 'expensive:endpoint' }
)
);Cacheability is enforced automatically:
- Only
GET/HEADrequests are cached - Requests with
Authorizationheaders are never cached - Requests with cookies are cached only if
Cache-Control: publicis sent - Responses with non-200 status or
Cache-Control: private/no-storeare never stored;set-cookieheaders are stripped before storing (so cookie-authenticated apps still cache)
defineCachedFunction()
Wrap an arbitrary function with caching. Useful for memoizing expensive computations (DB queries, remote fetches, derived data) with TTL- and tag-based invalidation.
import { defineCachedFunction, cacheLife, cacheTag } from 'ubean/server';
// Wrap a function; declare TTL and tags inside the function body
export const getUserProfile = defineCachedFunction(
async (userId: string) => {
cacheLife(60); // 60s TTL
cacheTag('users', `user:${userId}`); // attach tags for invalidation
const user = await db.query.users.findById(userId);
return user;
},
{ name: 'getUserProfile' } // name is required (cache key base)
);
// Invalidate by tag from a mutation handler
import { revalidateTag } from 'ubean/server';
await revalidateTag('users'); // invalidates every entry tagged 'users'| Option | Type | Description |
|---|---|---|
| name | string (required) | Cache key prefix (e.g. 'getUserProfile') |
| defaultTtl | number | TTL in seconds when cacheLife() is not called (default 60) |
| getKey | (…args) => string | Custom cache key generator (defaults to name + JSON.stringify(args)) |
invalidateRouteCache()
Invalidate cached entries by key, pattern, or clear all.
import { invalidateRouteCache } from 'ubean/server';
await invalidateRouteCache('GET:/api/users:1'); // exact key
await invalidateRouteCache(/^GET:\/api\/users:/); // regex match
await invalidateRouteCache(); // clear allRoute rules integration
Cache rules are usually declared in ubean.config.ts via routeRules. The runtime resolves them into CacheRules automatically.
// ubean.config.ts
import { defineConfig } from 'ubean';
export default defineConfig({
routeRules: {
'/api/products/**': { cache: { ttl: 60 } },
'/api/feed': { cache: { ttl: 300, swr: true } },
'/api/user/**': { headers: { 'cache-control': 'no-store' } }
}
});The middleware sets an X-Cache: HIT header and an Age header on cached responses for observability (a MISS marker is not emitted).
ISR (Incremental Static Regeneration)
ISR (P9-03) caches the rendered HTML of page routes with a TTL, optionally serving stale content while revalidating in the background. Unlike cache rules (which cache HTTP responses at the middleware layer), ISR caches the output of the Vue SSR renderer and is configured per-route via routeRules.isr.
// ubean.config.ts
export default defineConfig({
routeRules: {
// Regenerate /blog/** pages every 60s; serve stale while revalidating
'/blog/**': { isr: { ttl: 60, swr: true } },
// Simple form: ttl only (no SWR)
'/news/**': { isr: 300 }
}
});| Form | Behavior |
|---|---|
isr: 300 | Cache HTML for 300s. After expiry, the next request re-renders. |
isr: { ttl: 60 } | Same as isr: 60. |
isr: { ttl: 60, swr: true } | Cache HTML for 60s; after expiry, serve stale + revalidate in background. |
How it works
- On a
GETrequest to a page with an ISR rule, the router checks the cache store for the rendered HTML. - HIT (entry exists and not expired) → serve cached HTML with
X-ISR: HIT. - STALE (entry exists but expired,
swr: true) → serve stale HTML withX-ISR: STALE, trigger background revalidation (deduped per-path). - MISS (no entry, or
swr: falseand entry expired) → render HTML synchronously, cache it, serve withX-ISR: MISS.
The cache store is automatically initialized by the runtime when any isr rule is present. Custom stores should implement peek() to support SWR — otherwise ISR falls back to get() which deletes expired entries (disabling stale serving).
Invalidation
Use invalidateRouteCache() to invalidate ISR entries (same API as HTTP cache):
import { invalidateRouteCache } from 'ubean/server';
// After publishing a new blog post, invalidate all /blog/* ISR entries
await invalidateRouteCache(/^isr:\/blog\//);Relationship to other rendering modes
| Feature | When it runs | Output | Caching |
|---|---|---|---|
prerender: true | Build time | Static HTML file | Permanent (until next build) |
isr | First request / on expiry | Cached HTML | TTL-based + optional SWR |
ssr: true (default) | Every request | Fresh HTML | None |
ssr: false | Client-side | Empty shell | None |
ISR requires SSR to be enabled (either globally via ssr: true, or per-route via ssr: true / ssr: 'streaming'). A route with ssr: false and isr will skip ISR (no renderer available).
Custom stores
Implement CacheStore for Redis, Cloudflare KV, or any durable backend:
import { useCacheStore } from 'ubean/server';
import { type CacheStore } from 'ubean/server';
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const redisStore: CacheStore = {
async get(key) {
const raw = await redis.get(`cache:${key}`);
return raw ? JSON.parse(raw) : undefined;
},
async set(key, entry, ttl) {
await redis.set(`cache:${key}`, JSON.stringify(entry), { EX: ttl });
},
async delete(key) {
const count = await redis.del(`cache:${key}`);
return count > 0;
},
async clear() {
// implement carefully — usually scoped by prefix in production
}
};
useCacheStore(redisStore);What ubean does NOT provide
ubean does not ship these APIs (common in other frameworks’ cache modules):
useCache()/defineCache()— useuseCacheStore()+cachedEventHandlerinstead- Cache groups and
remember()/rememberForever()helpers — usedefineCachedFunction+revalidateTaginstead - Built-in Redis/Memcached/file drivers — implement the
CacheStoreinterface yourself, or mount a driver on ubean’s storage layer
For arbitrary application-level key/value caching (not HTTP response caching), prefer the built-in useStorage / useKV (from ubean/server):
import { useStorage } from 'ubean/server';
const storage = useStorage();
await storage.set('user:1', { name: 'John' });
const user = await storage.get('user:1');Best Practices
- Cache at the route level — prefer
routeRulesover per-handler caching for consistency. - Scope custom stores — namespace Redis keys to avoid collisions across deployments.
- Invalidate on mutation — call
invalidateRouteCache(...)fromPOST/PATCH/DELETEhandlers. - Never cache authed responses — the middleware enforces this, but verify your
Cache-Controlheaders. - Use
swrfor high-traffic feeds — keeps latency low while revalidating in the background.