NestJS — Enterprise Node.js

Caching with Redis

16 min Lesson 40 of 48

Caching with Redis

Caching is one of the most impactful performance techniques available to a backend engineer. Instead of recomputing or re-fetching data on every request, a cache stores the result and returns it instantly for subsequent calls. NestJS ships with a first-class CacheModule built on top of cache-manager, and with Redis as the backing store you get a distributed, persistent cache that survives restarts and scales across multiple application instances.

Installing dependencies

npm install @nestjs/cache-manager cache-manager npm install cache-manager-ioredis-yet ioredis npm install -D @types/cache-manager

cache-manager-ioredis-yet is the modern Redis store adapter for cache-manager v5+. It uses ioredis under the hood, which is the recommended Redis client for Node.js in production.

Registering CacheModule with Redis

Import CacheModule in your AppModule (or any feature module). Use CacheModule.registerAsync() to pull Redis connection details from ConfigService:

import { Module } from '@nestjs/common'; import { CacheModule } from '@nestjs/cache-manager'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { redisStore } from 'cache-manager-ioredis-yet'; @Module({ imports: [ ConfigModule.forRoot(), CacheModule.registerAsync({ isGlobal: true, imports: [ConfigModule], inject: [ConfigService], useFactory: async (config: ConfigService) => ({ store: redisStore, host: config.get<string>('REDIS_HOST', 'localhost'), port: config.get<number>('REDIS_PORT', 6379), ttl: 60, // default TTL in seconds }), }), ], }) export class AppModule {}
isGlobal: true makes the cache available across every module without re-importing CacheModule. For large apps this is almost always what you want.

The CacheInterceptor — zero-boilerplate route caching

NestJS provides CacheInterceptor which automatically caches the entire response of a controller method. Apply it at the controller or method level with @UseInterceptors, or bind it globally via the APP_INTERCEPTOR provider:

import { Controller, Get, UseInterceptors } from '@nestjs/common'; import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager'; @Controller('products') @UseInterceptors(CacheInterceptor) export class ProductsController { // Uses default TTL (60 s) and auto-generated key based on route URL @Get() findAll() { return this.productsService.findAll(); } // Override both key and TTL for this specific endpoint @Get('featured') @CacheKey('products:featured') @CacheTTL(300) // 5 minutes getFeatured() { return this.productsService.getFeatured(); } }

@CacheKey pins the cache entry to a fixed string key regardless of URL parameters. @CacheTTL overrides the module-level default for that handler only.

Manual cache operations via CACHE_MANAGER

For fine-grained control — writing, reading, or deleting individual keys — inject the cache manager token directly:

import { Injectable, Inject } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; @Injectable() export class ProductsService { constructor(@Inject(CACHE_MANAGER) private cache: Cache) {} async findById(id: number) { const key = `product:${id}`; const cached = await this.cache.get<Product>(key); if (cached) return cached; const product = await this.repo.findOne({ where: { id } }); await this.cache.set(key, product, 120); // TTL 120 s return product; } async update(id: number, dto: UpdateProductDto) { const product = await this.repo.save({ id, ...dto }); await this.cache.del(`product:${id}`); // invalidate on write return product; } }

Cache invalidation strategies

  • TTL-based expiry — the simplest strategy; the cache expires automatically after N seconds. Suitable when slightly stale data is acceptable (e.g., product listings).
  • Event-driven invalidation — call cache.del(key) explicitly when the underlying data changes (update/delete). Ensures the next read fetches fresh data immediately.
  • Namespace / tag-based invalidation — prefix related keys (e.g., products:*) and iterate to delete a group. Useful for cache-busting an entire resource family at once.
  • Write-through — update the cache and the database in the same operation so the cache is never stale; slightly more complex but guarantees consistency.
Never cache responses that contain user-specific data at a shared key. If two users share a cache key, user A may receive user B's private data. Always incorporate a user identifier into the key when the response is personalized (e.g., orders:${userId}).
Use Redis key prefixes in production. Set keyPrefix in the store config (e.g., 'myapp:') so your keys are namespaced away from other applications or services that share the same Redis instance.

Summary

CacheModule with a Redis store gives NestJS apps a distributed, persistent cache. CacheInterceptor handles automatic route-level caching; @CacheKey and @CacheTTL tune individual endpoints. For manual operations inject CACHE_MANAGER and call get, set, and del. Always choose an invalidation strategy — TTL, event-driven, or write-through — appropriate to your consistency requirements, and keep user-specific data behind user-scoped keys.