@nathapp/nestjs-cache
v3.0.0
Published
nestjs-cache
Readme
@nathapp/nestjs-cache
Enterprise-grade caching module for NestJS with multi-tier storage, tag-based invalidation, and distributed synchronization.
Built on top of Cacheable and Keyv for high-performance L1/L2 caching.
Features
- Multiple strategies: Memory, Distributed (Redis), Multi-Tier (L1 + L2), Legacy
- Tag-based invalidation: O(m) bulk cache clearing using Redis Sets
- Distributed sync: Automatic cross-instance cache synchronization via CacheableSync
- Unified API: Single
invalidate()method with mode overloads (key,pattern,tag) - Redis URI support: Parse
redis://andrediss://URIs into connection objects - Metrics hooks: Monitor hits, misses, sets, deletes, and errors
- Health checks: Built-in health check interface
- Namespace support: Isolated cache namespaces with auto-prefixed tags
- Graceful degradation: Fallback to memory when Redis is unavailable (MULTI_TIER)
- Non-blocking mode: Background L2 updates for low-latency responses
Installation
npm install @nathapp/nestjs-cache cacheable @keyv/redis
# For distributed sync (MULTI_TIER with multiple instances):
npm install @qified/redisQuick Start
Memory Strategy (Single Instance)
import { CacheModule, CacheStrategy } from '@nathapp/nestjs-cache';
@Module({
imports: [
CacheModule.register({
strategy: CacheStrategy.MEMORY,
memory: {
lruSize: 1000,
ttl: '5m',
},
}),
],
})
export class AppModule {}Distributed Strategy (Redis Only)
CacheModule.register({
strategy: CacheStrategy.DISTRIBUTED,
distributed: {
host: 'localhost',
port: 6379,
ttl: '10m',
},
})Multi-Tier Strategy (L1 Memory + L2 Redis)
CacheModule.register({
strategy: CacheStrategy.MULTI_TIER,
memory: {
lruSize: 500,
ttl: '1m',
},
distributed: {
url: 'redis://localhost:6379',
ttl: '10m',
},
})Redis URI Configuration
CacheModule.register({
strategy: CacheStrategy.DISTRIBUTED,
distributed: {
url: 'redis://username:[email protected]:6379/0',
ttl: '10m',
},
})Individual fields override URI-parsed values:
distributed: {
url: 'redis://localhost:6379',
port: 6380, // Overrides port from URI
}Usage
Basic Cache Operations
import { Injectable } from '@nestjs/common';
import { CacheManager } from '@nathapp/nestjs-cache';
@Injectable()
export class UserService {
constructor(private readonly cache: CacheManager) {}
async getUser(id: string) {
// Get with resolver (cache-aside pattern)
return this.cache.get(
['USER', id],
async () => this.userRepository.findById(id),
60000, // TTL in ms
);
}
async updateUser(id: string, data: UpdateUserDto) {
await this.userRepository.update(id, data);
// Invalidate cached user
await this.cache.invalidate(['USER', id]);
}
}Tag-Based Invalidation
Tags allow efficient bulk invalidation of related cache entries. Instead of scanning the entire keyspace (O(n)), tags use Redis Sets for O(m) complexity where m = number of tagged keys.
@Injectable()
export class PostService {
constructor(private readonly cache: CacheManager) {}
async getPost(userId: string, postId: string) {
return this.cache.get(
['USER', userId, 'POST', postId],
async () => this.postRepository.findOne(postId),
{ tags: ['USER:' + userId + ':POSTS', 'USER:' + userId] },
);
}
async setPost(userId: string, postId: string, data: Post) {
await this.cache.set(
['USER', userId, 'POST', postId],
data,
60000,
{ tags: ['USER:' + userId + ':POSTS', 'USER:' + userId] },
);
}
async deleteAllUserPosts(userId: string) {
// Invalidate all posts for a user in one call (O(m))
await this.cache.invalidate(
['USER', userId, 'POSTS'],
{ mode: 'tag' },
);
}
async deleteAllUserData(userId: string) {
// Invalidate everything for a user
await this.cache.invalidate(
['USER', userId],
{ mode: 'tag' },
);
}
}get() Overloads
// Basic get (returns cached value or null)
const value = await cache.get<User>(['USER', '123']);
// Get with resolver (cache-aside)
const user = await cache.get(['USER', '123'], () => fetchUser('123'));
// Get with resolver and explicit TTL
const user = await cache.get(['USER', '123'], () => fetchUser('123'), 60000);
// Get with resolver and tags (default 300s TTL)
const user = await cache.get(['USER', '123'], () => fetchUser('123'), {
tags: ['USER:123'],
});
// Get with resolver, TTL, and tags
const user = await cache.get(['USER', '123'], () => fetchUser('123'), 60000, {
tags: ['USER:123'],
});Unified invalidate() API
// Exact key deletion (default)
await cache.invalidate(['USER', '123']);
// Explicit mode: exact key
await cache.invalidate(['USER', '123'], { mode: 'key' });
// Tag-based invalidation (O(m))
await cache.invalidate(['USER:123:POSTS'], { mode: 'tag' });
// Backward compatibility: group=true now maps to tag mode
await cache.invalidate(['USER', '123'], true);Batch Invalidation
await cache.mInvalidate([
{ cacheKey: ['USER', '123'], mode: 'key' },
{ cacheKey: ['USER:456:POSTS'], mode: 'tag' },
{ cacheKey: ['SESSION:789'], mode: 'key' },
]);Namespaced Cache
const postCache = cache.namespace('POSTS');
// All keys are auto-prefixed: POSTS:123
await postCache.set(['123'], postData, 60000, {
tags: ['AUTHOR:456'], // Stored as POSTS:AUTHOR:456
});
// Invalidate within namespace
await postCache.invalidate(['AUTHOR:456'], { mode: 'tag' });
// Invalidate all keys in namespace
await postCache.invalidateAll();Metrics
CacheModule.register({
strategy: CacheStrategy.DISTRIBUTED,
distributed: { host: 'localhost', port: 6379 },
metrics: {
onCacheHit: (ctx) => console.log(`HIT ${ctx.key} ${ctx.durationMs}ms`),
onCacheMiss: (ctx) => console.log(`MISS ${ctx.key}`),
onCacheSet: (ctx) => console.log(`SET ${ctx.key} ttl=${ctx.ttl}`),
onCacheDelete: (ctx) => console.log(`DEL ${ctx.key}`),
onCacheError: (ctx) => console.error(`ERR ${ctx.key}`, ctx.error),
},
})Health Check
const health = await cache.isHealthy();
// {
// healthy: true,
// timestamp: Date,
// connections: [
// { name: 'memory', status: 'connected' },
// { name: 'redis', status: 'connected', latencyMs: 2 }
// ],
// stats: { ... }
// }Distributed Synchronization (CacheableSync)
For multi-instance deployments using MULTI_TIER, cache synchronization is automatically enabled. When one instance sets or deletes a key, all other instances update their L1 (memory) caches via Redis Pub/Sub.
CacheModule.register({
strategy: CacheStrategy.MULTI_TIER,
memory: { ttl: '1m' },
distributed: { host: 'localhost', port: 6379, ttl: '10m' },
// sync is enabled by default for MULTI_TIER
// To disable: sync: { enabled: false }
})To use a custom namespace for sync isolation:
sync: {
enabled: true,
namespace: 'my-service',
}Tag Configuration
CacheModule.register({
strategy: CacheStrategy.DISTRIBUTED,
distributed: { host: 'localhost', port: 6379 },
tags: {
enabled: true, // default: true when Redis available
tagPrefix: '_TAG:', // prefix for tag sets in Redis
tagTtl: 86400, // tag set TTL in seconds (24h)
maxTagsPerKey: 10, // max tags per cache entry
maxKeysPerTag: 10000, // safety limit per tag set
},
})Cache Interceptor
Automatically cache HTTP responses:
import { CacheInterceptor, CacheReqOptions } from '@nathapp/nestjs-cache';
@Controller('users')
@UseInterceptors(CacheInterceptor)
export class UserController {
@Get(':id')
@CacheReqOptions({ ttl: 60000 })
getUser(@Param('id') id: string) {
return this.userService.findById(id);
}
}Strategies Comparison
| Feature | MEMORY | DISTRIBUTED | MULTI_TIER | LEGACY | |---------|--------|------------|------------|--------| | L1 (Memory) | ✅ | ❌ | ✅ | ✅ | | L2 (Redis) | ❌ | ✅ | ✅ | ❌ | | Tag Invalidation | ❌ | ✅ | ✅ | ⚠️ Limited | | Cross-Instance Sync | N/A | N/A | ✅ Auto | ❌ | | Graceful Degradation | N/A | ✅ | ✅ | N/A | | Non-Blocking | N/A | N/A | ✅ | N/A | | Recommended | Dev/Test | Single Redis | Production | Migration only |
Migration from Legacy
Before (Legacy)
import { CacheModule } from '@nathapp/nestjs-cache';
CacheModule.register({
// No strategy = LEGACY by default
ttl: 300,
})After (Enhanced)
import { CacheModule, CacheStrategy } from '@nathapp/nestjs-cache';
CacheModule.register({
strategy: CacheStrategy.MULTI_TIER,
memory: { lruSize: 1000, ttl: '5m' },
distributed: { host: 'localhost', port: 6379, ttl: '10m' },
})The CacheManager API is backward compatible — get(), set(), invalidate() all work the same way with enhanced strategies.
Deprecated APIs
⚠️ The following APIs are deprecated and will be removed in a future major release. Please migrate to the recommended alternatives.
InvalidateStrategy.BROKER
Deprecated. Cross-instance cache invalidation is now handled by CacheableSync (built into Cacheable).
// ❌ Deprecated
CacheModule.register({
invalidate: {
strategy: InvalidateStrategy.BROKER,
transport: Transport.REDIS,
options: { host: 'localhost', port: 6379 },
},
})
// ✅ Replacement — use MULTI_TIER strategy (sync is automatic)
CacheModule.register({
strategy: CacheStrategy.MULTI_TIER,
distributed: { host: 'localhost', port: 6379 },
})CacheInvalidateManager / RedisCacheInvalidateManager
Deprecated. These classes are replaced by CacheableSync for enhanced strategies. They remain functional only for CacheStrategy.LEGACY.
invalidateByTag() / invalidateByTags()
Deprecated. Use the unified invalidate() API with { mode: 'tag' } instead.
// ❌ Deprecated
await cache.invalidateByTag('USER:123:POSTS');
await cache.invalidateByTags(['USER:123:POSTS', 'USER:456:POSTS']);
// ✅ Replacement
await cache.invalidate(['USER:123:POSTS'], { mode: 'tag' });
await cache.mInvalidate([
{ cacheKey: ['USER:123:POSTS'], mode: 'tag' },
{ cacheKey: ['USER:456:POSTS'], mode: 'tag' },
]);invalidateByPattern()
Deprecated. Pattern-based invalidation uses O(n) SCAN which doesn't scale. Use tag-based invalidation instead.
// ❌ Deprecated — O(n) keyspace scan
await cache.invalidateByPattern('USER:123:*');
// ✅ Replacement — O(m) tag-based
// First, set entries with tags:
await cache.set(['USER', '123', 'POST', '1'], data, 60000, {
tags: ['USER:123:POSTS'],
});
// Then invalidate by tag:
await cache.invalidate(['USER:123:POSTS'], { mode: 'tag' });Note: If
invalidateByPattern()is called and a tag registry is available, it will automatically redirect to tag-based invalidation with a deprecation warning.
buildRedisUrl()
Deprecated. Use resolveRedisConnection() to parse URIs into connection objects instead of building URL strings.
// ❌ Deprecated
const url = buildRedisUrl({ host: 'localhost', port: 6379 });
// ✅ Replacement
const connection = resolveRedisConnection({ host: 'localhost', port: 6379 });dipatch() (typo alias)
Deprecated. Use the correctly spelled dispatch() method.
lib/inteceptor/ (typo directory)
Deprecated. Imports from lib/inteceptor/ still work via barrel re-export but the implementation has moved to lib/interceptor/.
CacheStrategy.LEGACY
Deprecated. The legacy strategy using @nestjs/cache-manager is maintained for backward compatibility but will be removed in a future major release. Migrate to MEMORY, DISTRIBUTED, or MULTI_TIER.
API Reference
CacheManager
| Method | Description |
|--------|-------------|
| get<T>(keys, resolver?, ttlOrOptions?, options?) | Get or resolve+cache a value |
| getOnly<T>(keys) | Get without resolver |
| set<T>(keys, value, ttl?, options?) | Set a value with optional tags |
| mset<T>(list) | Set multiple values |
| mget(keys) | Get multiple values |
| invalidate(key, options?) | Unified invalidation (key/pattern/tag modes) |
| mInvalidate(list) | Batch invalidation with mixed modes |
| reset() | Clear all cache entries |
| touch(keys, ttl?) | Extend TTL of a cached item |
| namespace(name) | Create a namespaced cache manager |
| isHealthy() | Health check |
| getStats() | Cache statistics (enhanced mode) |
| isEnhancedMode() | Check if using cacheable |
| resolveKey(keyParts) | Resolve key parts to string |
CacheModule
| Method | Description |
|--------|-------------|
| register(options) | Synchronous registration |
| registerAsync(options) | Async registration with factory |
License
See the root monorepo license.
