Skip to content

Response Cache

@modularityjs/http-cache caches GET responses in CacheService and serves conditional requests with weak ETags — swap the cache driver and response caching becomes cross-instance.

@CacheResponse

typescript
import { CacheResponse } from '@modularityjs/http-cache';

@Controller('/products')
class ProductsController {
  @CacheResponse({ ttlMs: 30_000, varyHeaders: ['accept-language'] })
  @Get()
  list() {
    return this.catalog.expensiveListing();
  }
}
  • First GET: handler runs, the JSON result is stored for ttlMs, the response carries ETag and X-Cache: miss.
  • Subsequent GETs: served from the cache (X-Cache: hit) without invoking the handler.
  • A request whose If-None-Match matches the ETag gets a body-less 304.
  • Non-GET/HEAD requests and special return types (FileResponse, HtmlResponse, SseResponse, RedirectResponse, null/undefined) pass through uncached.

Options:

OptionPurpose
ttlMsFreshness window (required; validated at decoration time)
scopePer-caller cache-key dimension — see Private responses below
varyHeadersRequest headers folded into the cache key — keep the list short (cardinality)
keyPrefixCache-key namespace (default modularityjs:http-cache)
tagsPassed to CacheService.set — invalidate a group via CacheService.invalidateTag

Private responses

A cached response is keyed per caller so one user's private data is never served to another. By default the scope is the authenticated identity's id (request.auth, set by http-auth): an authenticated @Get('/me') @CacheResponse(...) caches separately per user. An authenticated request whose scope can't be resolved is not cached at all (fails safe), and an unauthenticated request caches under a shared key as before.

Override with scope to key on something else (or opt a route into shared caching by returning a constant):

typescript
@CacheResponse({
  ttlMs: 60_000,
  scope: (request) => request.headers['x-tenant-id'] as string | undefined,
})
@Get('/dashboard')
dashboard() { /* ... */ }

The interceptor also merges its varyHeaders into any Vary header set earlier in the request (e.g. Vary: Origin from @fastify/cors) rather than replacing it, so a shared cache / CDN keeps every dimension.

Wiring

typescript
// modules: [
//   ModularityModule,
//   HttpModule.forRoot({ port: 3000 }),
//   HttpFastifyModule,
//   CacheModule,
//   CacheMemoryModule,   // or CacheRedisModule — cache entries become cross-instance
//   HttpCacheModule,
//   ...
// ]

Like @SerializeWith, the decorator is a synthetic per-site interceptor on the UseInterceptor channel — zero adapter changes, portable to any HTTP driver. Using @CacheResponse without HttpCacheModule wired fails with a StateException naming the fix.

Invalidation

Pass tags and call CacheService.invalidateTag('products') after a write, or use a keyPrefix per resource family and delete by key. The cache key is prefix|METHOD|url|scope|varyValues (the scope segment is omitted when there is none), so distinct callers and distinct query strings are distinct entries.