Service API
Direct cache manipulation when decorators aren't enough.
Inject CacheService
import { Injectable } from '@nestjs/common';
import { CacheService } from '@nestjs-redisx/cache';
@Injectable()
export class UserService {
constructor(private readonly cache: CacheService) {}
}Or by token (useful for mocking in tests):
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_SERVICE, type ICacheService } from '@nestjs-redisx/cache';
@Injectable()
export class UserService {
constructor(@Inject(CACHE_SERVICE) private readonly cache: ICacheService) {}
}Basic Operations
Get
const user = await this.cache.get<User>('user:123');
// Returns: User | nullSet
// Simple set
await this.cache.set('user:123', user);
// With options
await this.cache.set('user:123', user, {
ttl: 300,
tags: ['users', 'user:123'],
strategy: 'l1-l2', // 'l1-only' | 'l2-only' | 'l1-l2' (default)
varyBy: { tenantId: 'acme' }, // Additional key variation (multi-tenant)
});varyBy in Service API vs decorators
In Service API, varyBy accepts resolved key-value pairs (Record<string, string>) — you provide the values directly. In @Cached decorator, varyBy accepts key names (string[]) — values are resolved automatically from contextProvider.
Get or Set (Cache-Aside)
Atomically get from cache or load and cache if missing. Includes anti-stampede protection.
const user = await this.cache.getOrSet<User>(
'user:123',
() => this.repository.findOne('123'),
{ ttl: 300, tags: ['users'] }
);Full options (inherits all set() options plus SWR and stampede control):
const user = await this.cache.getOrSet<User>(
'user:123',
() => this.repository.findOne('123'),
{
ttl: 300,
tags: ['users'],
strategy: 'l1-l2', // 'l1-only' | 'l2-only' | 'l1-l2'
varyBy: { tenantId: 'acme' }, // Additional key variation
swr: { enabled: true, staleTime: 60 }, // Stale-while-revalidate
skipStampede: true, // Bypass anti-stampede for this call
unless: (result) => result === null, // Don't cache null results
}
);Hashing Objects into Keys
When the cache key is "this whole request body", use hashKey() — the same frozen algorithm the @Cached decorator uses for auto-generated keys: object keys are sorted recursively (so {a:1,b:2} and {b:2,a:1} produce the same key), the result is SHA-256 truncated to 16 hex chars. No hand-rolled "sort keys then hash" helpers needed. Maps and Sets are serialized deterministically (sorted, type-prefixed), so distinct Maps/Sets produce distinct keys.
import { Injectable } from '@nestjs/common';
import { CacheService, hashKey, KeyBuilder } from '@nestjs-redisx/cache';
import { CalculateRequestDto } from './types';
@Injectable()
export class CalculationService {
constructor(private readonly cache: CacheService) {}
async calculate(body: CalculateRequestDto): Promise<unknown> {
// hashKey: stable hash of the whole DTO — {a:1,b:2} and {b:2,a:1}
// produce the SAME key (object keys are sorted recursively).
const key = `calc:${hashKey(body)}`;
return this.cache.getOrSet(key, () => this.compute(body), { ttl: 300 });
}
async invalidate(body: CalculateRequestDto): Promise<void> {
// The hash is deterministic, so the same body always maps to the
// same key — usable for targeted deletes too.
await this.cache.del(`calc:${hashKey(body)}`);
}
buildVersionedKey(body: CalculateRequestDto): string {
// Same algorithm via the fluent builder.
return KeyBuilder.create().prefix('calc').version('v2').hashStable(body).build();
// 'calc:v2:<16-hex-hash>'
}
private async compute(body: CalculateRequestDto): Promise<unknown> {
return { input: body };
}
}The fluent builder offers the same algorithm as KeyBuilder.hashStable().
Stability contract
The hashKey algorithm is frozen — its output is part of the public API, because your cache keys are derived from it. It will never change in place (a change would silently invalidate every derived key on upgrade); any future algorithm ships under a new name. KeyBuilder.hash() is deprecated: it is key-order-sensitive and 32-bit (collision-prone) — prefer hashStable() / hashKey(). Its output is intentionally unchanged for backward compatibility. Scope note: hashKey is for cache keys, not identity or security.
Delete
// Single key
const deleted = await this.cache.del('user:123');
// Returns: boolean
// Multiple keys
const count = await this.cache.deleteMany(['user:123', 'user:456']);
// Returns: number (count of deleted keys)Has
const exists = await this.cache.has('user:123');
// Returns: booleanTTL
const ttl = await this.cache.ttl('user:123');
// Returns: number (seconds), -1 if no TTL, -2 if key doesn't existBatch Operations
Batch Get
const users = await this.cache.getMany<User>([
'user:1',
'user:2',
'user:3',
]);
// Returns: Array<User | null>Batch Set
await this.cache.setMany([
{ key: 'user:1', value: user1, ttl: 3600, tags: ['users'] },
{ key: 'user:2', value: user2, ttl: 3600, tags: ['users'] },
{ key: 'user:3', value: user3, ttl: 3600, tags: ['users'] },
]);INFO
setMany accepts key, value, ttl, and tags per entry. For strategy per entry, use individual set() calls.
Tag Operations
Get Keys by Tag
const keys = await this.cache.getKeysByTag('users');
// Returns: string[]Invalidate by Tag
// Single tag
const count = await this.cache.invalidate('users');
// Returns: number (count of invalidated keys)
// Multiple tags
const count = await this.cache.invalidateTags(['users', 'products']);
// Returns: number (total keys invalidated)Invalidate by Pattern
// Delete all keys matching pattern (uses Redis SCAN)
const count = await this.cache.invalidateByPattern('user:*');
// Returns: number (count of deleted keys)Wrap Function
Wrap any function with caching logic. Uses getOrSet internally (includes anti-stampede).
import { Injectable } from '@nestjs/common';
import { CacheService } from '@nestjs-redisx/cache';
import { UserRepository } from './types';
@Injectable()
export class UserService {
constructor(
private readonly cache: CacheService,
private readonly repository: UserRepository,
) {}
getById = this.cache.wrap(
async (id: string) => this.repository.findById(id),
{
key: (id: string) => `user:${id}`, // Key builder (required, function)
ttl: 3600, // TTL in seconds
tags: (id: string) => [`user:${id}`, 'users'], // Static array or function
}
);
}Clear All
// Use with caution in production
await this.cache.clear();Statistics
const stats = await this.cache.getStats();
/*
{
l1: {
hits: 15234,
misses: 1876,
size: 523,
},
l2: {
hits: 45123,
misses: 2341,
},
stampedePrevented: 142,
}
*/Error Handling
Read operations (get, has, ttl, getMany, getKeysByTag) are fail-open — errors return null/false/[].
Write operations (set, del, deleteMany, setMany, getOrSet, invalidate, clear) are fail-closed — errors throw.
| Scenario | Behavior |
|---|---|
Redis error on get / has / ttl | Fail-open. Returns null / false / -1. Error logged. |
Redis error on set / del / clear | Fail-closed. Throws CacheError. |
Key validation fails on get / has / ttl | Fail-open. Returns null / false / -1. Warning logged. |
Key validation fails on set / del / getOrSet | Fail-closed. Throws CacheKeyError. |
Loader throws in getOrSet | Error propagates. Cache is not updated. |
invalidate / invalidateTags fails | Fail-closed. Throws CacheError. |
invalidateByPattern fails | Fail-closed. Throws CacheError. |
Key validation rules: non-empty, no whitespace, only a-zA-Z0-9_-:., max 1024 chars (configurable via keys.maxLength).
Next Steps
- Tag Invalidation — Advanced tag patterns
- Anti-Stampede — Prevent cache stampede