@yonastewabe/nestjs-redis-cache
v1.0.1
Published
Generic, zero-app-specific Redis cache module for NestJS, built on ioredis, with an HTTP interceptor and TypeORM auto-invalidation
Downloads
280
Maintainers
Readme
@yonastewabe/redis-cache
Fully generic, zero-app-specific Redis cache module for NestJS.
Built on raw ioredis — no @nestjs/cache-manager involved.
What's included
| File | Purpose |
|---|---|
| RedisCacheService | ioredis client — get / set / del / pattern-delete / entity invalidation |
| RedisCacheInterceptor | HTTP interceptor — cache-aside for GET, invalidation for mutations |
| CacheInvalidationSubscriber | TypeORM subscriber — auto-invalidates on insert / update / soft-delete |
| RedisCacheModule | NestJS module with forRoot(options) and forFeature(options) |
| BaseModel | Generic TypeORM base entity (extend or replace with your own) |
| redisConfiguration | Config factory for ConfigModule.forRoot({ load: [...] }) |
| RedisCacheModuleOptions | TypeScript interface for all available options |
Installation
npm install ioredis typeorm @nestjs/common @nestjs/config @nestjs/core rxjs reflect-metadataCopy (or symlink) this folder into your project, or reference it locally:
"@yonastewabe/redis-cache": "file:../redis"Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
| REDIS_ENABLED | yes | — | Set to "true" to enable. Any other value → safe no-op. |
| REDIS_HOST | yes | — | Redis hostname |
| REDIS_PORT | yes | 6379 | Redis port |
| REDIS_PASSWORD | no | — | Redis password (omit if no auth) |
Copy .env.example to .env and fill in your values.
Quick start
1. Register the module
// app.module.ts
import { ConfigModule } from '@nestjs/config';
import { RedisCacheModule, redisConfiguration } from '@ie/redis-cache';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [redisConfiguration],
// If you already have a config factory, just merge the `redis` block
// from redis.config.ts into it instead of loading redisConfiguration.
}),
RedisCacheModule.forRoot({
// All options are optional — see full reference below
excludeRoutes: ['/health', '/metrics'],
excludedEntities: ['AuditLog', 'ActivityFeed'],
entityRouteMap: {
Product: 'products',
OrderItem: 'order-items',
},
defaultTtl: 3600,
ttlOverrides: { 'live-feed': 30 },
tenantIdHeader: 'x-tenant-id',
invalidationDelayMs: 50,
}),
],
})
export class AppModule {}2. Register the HTTP interceptor globally
// main.ts
import { RedisCacheInterceptor } from '@ie/redis-cache';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Interceptor is already configured via forRoot options — just register it
const interceptor = app.get(RedisCacheInterceptor);
app.useGlobalInterceptors(interceptor);
await app.listen(3000);
}3. Wire the TypeORM subscriber
TypeORM instantiates subscribers before NestJS DI is ready, so RedisCacheService
must be wired in manually after app.init():
// main.ts
import { DataSource } from 'typeorm';
import { RedisCacheService, CacheInvalidationSubscriber } from '@ie/redis-cache';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.init();
const dataSource = app.get(DataSource);
const redisCacheService = app.get(RedisCacheService);
const sub = dataSource.subscribers.find(
(s) => s instanceof CacheInvalidationSubscriber,
) as CacheInvalidationSubscriber;
if (sub) sub.setCacheService(redisCacheService);
await app.listen(3000);
}Also add CacheInvalidationSubscriber to your TypeORM config:
TypeOrmModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
// ...
subscribers: [CacheInvalidationSubscriber],
}),
})4. Inject the service anywhere
import { RedisCacheService } from '@ie/redis-cache';
@Injectable()
export class MyService {
constructor(private readonly cache: RedisCacheService) {}
async example() {
await this.cache.set('my-key', JSON.stringify({ hello: 'world' }), 300);
const value = await this.cache.get('my-key');
await this.cache.del('my-key');
await this.cache.deleteKeysByPattern('products:*tenant:abc*');
await this.cache.invalidateEntityCache('products', tenantId, entityId);
}
}Using your own base entity
If your project already has a base entity, pass it via baseEntity:
// your-base.entity.ts
@Entity()
export class AppBaseEntity {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid' }) tenantId: string; // required for tenant-scoped invalidation
@CreateDateColumn() createdAt: Date;
@UpdateDateColumn() updatedAt: Date;
@DeleteDateColumn() deletedAt?: Date;
}
// app.module.ts
RedisCacheModule.forRoot({
baseEntity: AppBaseEntity,
})The subscriber will listen to AppBaseEntity instead of the built-in BaseModel.
All available options
RedisCacheModule.forRoot({
/**
* Base entity class the subscriber listens to.
* Default: built-in BaseModel
*/
baseEntity: MyBaseEntity,
/**
* Maps entity class names → API route prefixes for cache key matching.
* Unmapped entities fall back to automatic kebab-case (OrderItem → order-item).
* Default: {}
*/
entityRouteMap: {
Product: 'products',
OrderItem: 'order-items',
},
/**
* Entity class names to skip during cache invalidation.
* Default: []
*/
excludedEntities: ['AuditLog', 'ActivityFeed'],
/**
* Route substrings the HTTP interceptor should never cache.
* Matched case-insensitively.
* Default: []
*/
excludeRoutes: ['/health', '/metrics'],
/**
* Default TTL in seconds for cached GET responses.
* Default: 43200 (12 hours)
*/
defaultTtl: 3600,
/**
* Per-route TTL overrides. Keys are route substrings, values are seconds.
* Default: {}
*/
ttlOverrides: { 'live-feed': 30, 'reports': 3600 },
/**
* Header name used to extract the tenant ID (case-insensitive).
* Default: 'tenantid'
*/
tenantIdHeader: 'x-tenant-id',
/**
* Delay in ms before cache invalidation runs after a DB write.
* Ensures the transaction has committed first.
* Default: 50
*/
invalidationDelayMs: 50,
})Cache key format
{normalizedRoute}:tenant:{tenantId}:params:{k}:{v}:query:{k}:{v}Route normalization strips /api/v1/ prefixes and replaces / with :.
Merging with an existing config factory
If your project already has a configuration.ts, add the redis block instead of
loading redisConfiguration separately:
export const configuration = () => ({
// ...your existing config...
redis: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
password: process.env.REDIS_PASSWORD,
enabled: process.env.REDIS_ENABLED === 'true',
},
});