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!