Skip to content

Response Caching

cacheResponse() is the middleware you reach for when a route is expensive, the result is shared across many clients, and you want to skip recomputation between requests. It sits in front of your handler, returns a stored response on a hit, and stores 2xx responses on a miss.

On every request the middleware computes a cache key — a static string or a function of the context — namespaces it by app and tenant, and looks it up in the configured cache backend. A hit returns immediately with x-cache: HIT and the original status, headers, and body. A miss runs your handler and stores the response if the status is 2xx; non-2xx responses are never cached. The default backend is whatever is set in ctx.config.resolvedStores.cache, typically Redis when you have it configured and falling back to in-memory.

Tenant scoping is automatic: when c.get('tenantId') is set, the key is prefixed with the tenant ID so two tenants can never read each other’s cached responses. Sensitive headers (set-cookie, authorization, CSRF tokens, www-authenticate) are stripped before storage to prevent session fixation.

src/catalog/package.ts
import { cacheResponse, definePackage, domain, route } from '@lastshotlabs/slingshot';
export const catalogPackage = definePackage({
name: 'catalog',
middleware: {
cacheList: cacheResponse({ key: 'products:list', ttl: 60 }),
cacheItem: cacheResponse({ key: c => `product:${c.req.param('id')}`, ttl: 300 }),
},
domains: [
domain({
name: 'products',
basePath: '/products',
routes: [
route.get({
path: '/',
middleware: ['cacheList'],
handler: async ({ respond }) => respond.json([]),
}),
route.get({
path: '/:id',
middleware: ['cacheItem'],
handler: async ({ respond }) => respond.json({}),
}),
],
}),
],
});

ttl is seconds. Omit it for indefinite caching — useful when paired with explicit invalidation. store overrides the backend per route if you need to pin a route to memory or Redis specifically.

When the underlying data changes you bust the cache by exact key or glob pattern. Pass the app reference so the helper can find the right adapter:

src/catalog/package.ts
import { z } from 'zod';
import { bustCache, bustCachePattern, definePackage, domain, route } from '@lastshotlabs/slingshot';
export const writePackage = definePackage({
name: 'catalog-writes',
domains: [
domain({
name: 'products',
basePath: '/products',
routes: [
route.put({
path: '/:id',
request: {
params: z.object({ id: z.string() }),
body: z.object({ name: z.string() }),
},
handler: async ({ params, body, request, respond }) => {
await updateProduct(params.id, body);
await bustCache(`product:${params.id}`, request.get('app'));
await bustCachePattern('products:list*', request.get('app'));
return respond.json({ ok: true });
},
}),
],
}),
],
});
declare function updateProduct(id: string, body: { name: string }): Promise<void>;

bustCache removes one entry. bustCachePattern removes everything matching a glob — useful when a write invalidates several derived list views.