Skip to content

Cache API ​

Import path: @sdcorejs/nestjs/services

The cache package provides an in-process LRU backend, an optional Redis backend, a resilient cache-aside service and a security-scoped HTTP handler decorator/interceptor.

Exports ​

ExportKindPurpose
CacheBackendinterfaceAsync get, set, del, clear, size contract
MemoryCacheBackendclassBounded in-process LRU with per-entry TTL
RedisCacheBackendclassJSON-serialized Redis backend with prefix-scoped scans
InvalidRedisCacheKeyPrefixErrorclassUnsafe/missing Redis namespace error
CacheServiceclassResilient DI-facing backend and cache-aside API
CacheModuleclassGlobal provider module; forRoot(config?)
CacheConfig, MemoryCacheConfig, RedisCacheConfig, RedisCacheOptionstypesBackend configuration contracts
CacheBackendKindtype'memory' | 'redis'
CACHE_CONFIGvalueResolved config DI token
Cached, CachedOptionsdecorator/typeMarks a handler with mandatory cache scope
CACHED_METADATAvalueDecorator metadata key
CacheScopetype'global' | 'tenant' | 'user'
CacheKeyValue, CacheKeyResolvertypesJSON-safe business-key material and selector
CacheHttpRequestDescriptorinterfaceNormalized method/URL/path/params/query/body key input
CacheInterceptorclassExecutes @Cached behavior; must be registered explicitly
RequestCacheMiddlewareclassAdds a fresh requestCache: Map to each request

Configure the backend ​

ts
CacheModule.forRoot({
  backend: 'memory',
  ttl: 60,
  maxEntries: 1_000,
  operationTimeoutMs: 1_000,
});
ts
CacheModule.forRoot({
  backend: 'redis',
  ttl: 300,
  operationTimeoutMs: 1_000,
  fallbackToMemory: false,
  redis: {
    host: 'redis.internal.example',
    port: 6379,
    db: 2,
    keyPrefix: 'orders:cache:',
  },
});
OptionDefaultNotes
backend'memory'Redis requires redis options and runtime ioredis
ttl60 seconds0 disables expiration
maxEntries1000Memory only; oldest-touched entry is evicted
fallbackToMemorytrueRedis construction failure only; invalid prefix still throws
operationTimeoutMs10000 disables timeout
redis.keyPrefixrequiredNonblank; cannot contain *, ?, [, ], or \

Redis clear() and size() use SCAN MATCH <keyPrefix>*; they never call FLUSHDB. Use a unique application/environment prefix. Values must be JSON-serializable.

CacheService ​

ts
const order = await cache.load(
  `order:${orderId}`,
  () => repository.findOneByOrFail({ id: orderId }),
  120,
);

await cache.set('catalog:version', 42, 0);
await cache.del(`order:${orderId}`);
MemberSignature / behavior
backendKindResolved 'memory' or 'redis', including fallback
get<T>(key) => Promise<T | undefined>; concurrent reads share one backend call
set<T>(key, value, ttlSec?) => Promise<void>
del(key) => Promise<void>
clear() => Promise<void>
size() => Promise<number>
load<T>(key, factory, ttlSec?) => Promise<T>; concurrent misses share one factory

Backend errors and timeouts are logged and degrade to miss/no-op/zero. Factory errors from load are not swallowed. undefined factory results are never cached. Single-flight locks are keyed only by key, so do not use different factories for the same key.

Register CacheInterceptor ​

CacheModule registers and exports CacheInterceptor, but intentionally does not apply it. Without one of the registrations below, @Cached() is metadata only and the handler always runs.

Per controller or handler:

ts
@UseInterceptors(CacheInterceptor)
@Controller('reports')
export class ReportsController {}

Global:

ts
@Module({
  providers: [
    { provide: APP_INTERCEPTOR, useExisting: CacheInterceptor },
  ],
})
export class AppModule {}

@Cached scopes and keys ​

ts
@Get(':id')
@Cached({
  scope: 'tenant',
  ttl: 120,
  keyResolver: ({ params, query }) => ({ id: params.id, locale: query.locale }),
})
detail() {}

CachedOptions.scope is required:

ScopeRequired contextIsolation material
globalnoneExplicit global namespace only
tenantnon-empty tenantEvery JSON-safe context field except request/response/token/user and userId
usernon-empty tenant and userIdEvery JSON-safe context field except request/response/token/user

Roles and permissions are sorted before hashing. Custom and declaration-merged context fields are included. Missing identity, circular/unsafe values, unsupported transports or an exception in keyResolver cause a cache bypass, never a shared fallback key.

The final key is <Class>.<method>:<scopeHash>:<businessHash>. By default, business material is a normalized HTTP descriptor containing method, URL, path, params, query and body. Headers and raw Nest request/response objects are excluded. A custom resolver selects only the business suffix and cannot remove the mandatory scope namespace.

@Cached supports single-value request/response handlers. It uses the first Observable emission; multi-emission streams are unsupported, handlers that emit nothing fail with RxJS EmptyError, and undefined is not cached.

Global scope

Use scope: 'global' only when the response is identical for every tenant, user, role and permission set. The interceptor cannot infer whether application data is safe to share globally.

Request-local cache ​

RequestCacheMiddleware only creates the map; apply it through your own NestModule.configure:

ts
configure(consumer: MiddlewareConsumer): void {
  consumer.apply(RequestCacheMiddleware).forRoutes('*');
}

This map is not a replacement for authorization checks and disappears after the request.

Pending-work deduplication uses SingleFlightService from @sdcorejs/nestjs/utils. See cache and non-cache examples.

Released under the MIT License.