Files
What you'll learn
- Register CacheModule with a default ttl and explain what is stored and where
- Cache a GET route with CacheInterceptor, choose its key with @CacheKey, and invalidate it on a write
- Cache by hand with CACHE_MANAGER: get, set with a ttl, del and clear
Caching
The shelter's statistics page runs a full scan of the cats table every time anyone opens it, and the page is open on a screen in the lobby refreshing every few seconds. The numbers change a few times a day. Computing them a thousand times a day for the same answer is waste, and under load it is an outage. A cache keeps a computed answer for a while and hands it out again, and the whole craft is in "a while": how long, keyed by what, and thrown away when. Nest ships a cache module that does the mechanical part two ways, automatically for a route and by hand for anything else.
CacheModule
@nestjs/cache-manager wraps the cache-manager library, which in turn stores through Keyv, a key-value abstraction with an in-memory store by default and Redis and others a package away:
@Module({
imports: [CacheModule.register()],
controllers: [AppController],
})
export class AppModule {}
register({ ttl: 5000 }) sets the default time to live in milliseconds, after which an entry is dropped; 0 means never. isGlobal: true makes the cache available everywhere, and registerAsync builds the options from configuration. Switching to Redis is a stores option, not a code change, which is the reason to go through the module rather than a Map.
Caching a route automatically
CacheInterceptor caches a GET handler's response by the request URL and serves it from the cache until it expires:
@Controller()
@UseInterceptors(CacheInterceptor)
export class AppController {
@Get()
findAll(): string[] {
return [];
}
}
Only GET is cached; a POST is a change and goes through. Two decorators tune a route: @CacheKey('custom_key') replaces the URL as the key, and @CacheTTL(2000) overrides the module's default for that route, in milliseconds. Registered under APP_INTERCEPTOR the interceptor covers every GET in the application, which is more than most APIs want; per route is the usual choice. A subclass can override trackBy() to compute keys from headers or the user.
Caching by hand
The interceptor is a blunt instrument: the whole response, keyed by URL. For anything finer, inject the cache manager and use it:
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
const value = await this.cacheManager.get('key'); // undefined when absent
await this.cacheManager.set('key', 'value', 1000); // ttl in milliseconds; 0 never expires
await this.cacheManager.del('key');
await this.cacheManager.clear();
The pattern is called cache-aside: look the key up, and on a miss compute, store, return. It costs four lines and gives you the key, the lifetime and the moment of invalidation.
Invalidation
A cache is a promise to serve old data, so the question is not whether the data will be stale but for how long, and who fixes it. The ttl bounds the staleness. Explicit invalidation ends it early: when something changes the underlying data, delete the entries it affects. With the interceptor that needs a key you chose with @CacheKey, because you cannot otherwise know what URL to delete. Deleting too much is safe and cheap; deleting too little is a bug report.
Your task
The statistics and each cat's facts are computed once and served many times.
CatsModuleregisters a cache with a default time to live of one minute.GET /cats/statsis cached automatically under the keycats-stats;POST /catsdrops that entry, since a new cat changes the statistics.GET /cats/:id/factscaches by hand underfacts:<id>for ten seconds.DELETE /cats/cacheclears everything.
Each response says how many times it was computed. Run the starter and watch the number climb with every request; then run the solution and watch it stay put until something changes.
When it fails
Nest can't resolve dependencies of the CatsController (?, CatsService), argumentCACHE_MANAGER:CacheModuleis not imported by the module that injects it.computedclimbs on every request to/cats/stats: the interceptor is missing, or the handler is not aGET.- After
POST /catsthe statistics are stale: the entry was deleted under a different key than the one the interceptor used; without@CacheKeythe key is the URL. factsis recomputed every time: the miss path stores under one key and the lookup reads another, orsetis never awaited before returning.
Remember
CacheModule.register({ ttl })provides the cache;ttlis in milliseconds,0is forever.@UseInterceptors(CacheInterceptor)caches aGETby URL;@CacheKeyand@CacheTTLtune it.@Inject(CACHE_MANAGER) cache: Cacheforget,set(key, value, ttl),delandclear, the cache-aside pattern.- Invalidate on writes, under the key you chose.
Stuck? Show a hint
Module: imports: [..., CacheModule.register({ ttl: 60_000 })] from '@nestjs/cache-manager'. Controller: @Inject(CACHE_MANAGER) private readonly cache: Cache (type from 'cache-manager'); @UseInterceptors(CacheInterceptor) @CacheKey('cats-stats') on stats(); await this.cache.del('cats-stats') after creating; facts: const cached = await this.cache.get<Facts>(key); if (cached) return cached; ... await this.cache.set(key, facts, 10_000); clear: await this.cache.clear().
Press Run tests to start the app. Its log appears here.Graded endpoints
A cat for the statistics
Computed once and stored by the interceptor under cats-stats
Served from the cache: the same response, and computed is still 1
A write: the handler drops the cached statistics
Recomputed, because the entry was invalidated under the key the interceptor uses
A miss: computed and stored under facts:1
A hit: the stored value, computed still 1
Everything forgotten
A miss again: computed twice now
The interceptor's entry went with the rest