Redis Caching in NestJS: A Practical Guide
Why Cache with Redis?
If you're building a NestJS app that needs to handle lots of requests or has expensive database queries, you're gonna want caching. Redis is perfect for this - it's blazing fast, easy to use, and NestJS has built-in support for it. Let me show you how to set it up!
Setting Up Redis
First, let's install the required packages:
npm install @nestjs/cache-manager cache-manager cache-manager-redis-store redis
If you're using TypeScript (and you probably are), you'll also want the types:
npm install -D @types/cache-manager-redis-store
Basic Setup
First, register the cache module in your app.module.ts
:
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import * as redisStore from 'cache-manager-redis-store';
@Module({
imports: [
CacheModule.register({
store: redisStore,
host: 'localhost',
port: 6379,
ttl: 60 * 60, // Time to live in seconds (1 hour)
}),
],
})
export class AppModule {}
Pro Tip: In production, you'll want to load Redis config from environment variables. I'll show you how later!
Basic Caching
Here's a simple example using caching in a service:
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
@Injectable()
export class UserService {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache
) {}
async getUser(id: number) {
// Try to get user from cache first
const cachedUser = await this.cacheManager.get(`user-${id}`);
if (cachedUser) {
return cachedUser;
}
// If not in cache, get from database
const user = await this.findUserInDb(id);
// Store in cache for next time
await this.cacheManager.set(`user-${id}`, user);
return user;
}
}
Using Cache Decorators
NestJS provides some cool decorators to make caching even easier:
import { CacheKey, CacheTTL, UseInterceptors, CacheInterceptor } from '@nestjs/common';
@Controller('users')
@UseInterceptors(CacheInterceptor)
export class UserController {
@Get(':id')
@CacheKey('user-profile')
@CacheTTL(30) // Cache for 30 seconds
async getUser(@Param('id') id: number) {
return this.userService.getUser(id);
}
}
Advanced Caching Patterns
Here are some real-world patterns I use all the time:
1. Cache with Dynamic Keys
@Injectable()
export class ProductService {
@Inject(CACHE_MANAGER) private cacheManager: Cache;
async getProducts(category: string, page: number) {
const cacheKey = `products-${category}-${page}`;
const cached = await this.cacheManager.get(cacheKey);
if (cached) return cached;
const products = await this.findProducts(category, page);
await this.cacheManager.set(cacheKey, products, 1800); // 30 minutes
return products;
}
}
2. Cache Invalidation
@Injectable()
export class ProductService {
async updateProduct(id: number, data: UpdateProductDto) {
// Update in database
const updated = await this.productRepo.update(id, data);
// Invalidate related caches
await this.cacheManager.del(`product-${id}`);
await this.cacheManager.del('products-list');
return updated;
}
}
3. Batch Cache Operations
@Injectable()
export class CacheService {
async clearProductCaches(productIds: number[]) {
const keys = productIds.map(id => `product-${id}`);
await Promise.all(keys.map(key => this.cacheManager.del(key)));
}
async getCachedProducts(ids: number[]) {
const keys = ids.map(id => `product-${id}`);
const cached = await Promise.all(
keys.map(key => this.cacheManager.get(key))
);
return cached.filter(Boolean);
}
}
Production Setup
Here's how to set up Redis caching for production:
// config/cache.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('cache', () => ({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
password: process.env.REDIS_PASSWORD,
ttl: parseInt(process.env.CACHE_TTL, 10) || 3600,
max: parseInt(process.env.CACHE_MAX, 10) || 100,
}));
// app.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config';
import cacheConfig from './config/cache.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [cacheConfig],
}),
CacheModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
store: redisStore,
host: configService.get('cache.host'),
port: configService.get('cache.port'),
password: configService.get('cache.password'),
ttl: configService.get('cache.ttl'),
max: configService.get('cache.max'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
Best Practices
- Use meaningful cache keys that include relevant parameters
- Set appropriate TTL values based on how often your data changes
- Implement cache invalidation when data is updated
- Use environment variables for Redis configuration
- Add error handling for cache operations
Error Handling
Always handle cache errors gracefully:
@Injectable()
export class UserService {
async getUser(id: number) {
try {
const cached = await this.cacheManager.get(`user-${id}`);
if (cached) return cached;
} catch (error) {
// Log error but don't fail the request
console.error('Cache error:', error);
// Continue without cache
}
// Fallback to database
return this.findUserInDb(id);
}
}
Monitoring and Debugging
Here's a simple cache monitor service I use in development:
@Injectable()
export class CacheMonitorService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async getCacheStats() {
const store = this.cacheManager.store;
return {
keys: await store.keys(),
memory: await store.getStats(),
};
}
async clearAll() {
return this.cacheManager.reset();
}
}
Quick Tip: In development, I often set shorter TTL values (like 30 seconds) to make testing easier. Just remember to update them for production!
Common Gotchas
- Circular Dependencies: Be careful when caching objects with circular references - they won't serialize properly
- Memory Usage: Set reasonable TTL and max values to prevent Redis from using too much memory
- Cache Stampede: When cache expires, multiple requests might try to rebuild it simultaneously. Consider implementing a cache lock pattern
- Type Safety: Redis stores everything as strings, so make sure to handle type conversion properly
Wrapping Up
That's it! You now have a solid foundation for implementing Redis caching in your NestJS app. Remember, caching is an optimization - start simple and add complexity only when needed. If you run into issues or have questions, check out the NestJS caching docs or drop a comment below!