The Cache Module

CacheModule gives you one cache object under the CACHE_MANAGER token: wrap a slow call, and pick a key you can delete later.

SyntaxCacheModule.register({ ttl?: number, stores?: Keyv[], isGlobal?: boolean })

A cache is a key-value store you check before doing slow work. CacheModule builds one and registers it as a provider — an object Nest creates once and hands to any class that asks for it. Providers are found by a token, the key the container files them under. This one's token is the string constant CACHE_MANAGER.

Registering it

Install @nestjs/cache-manager, cache-manager and keyv. NestJS 12 pairs with @nestjs/cache-manager 12 (the previous release line was 3.x) and requires cache-manager 6 or later and keyv 5 or later. CacheModule.register() returns a module you list in imports. registerAsync() does the same but builds the options in a factory, so a TTL or a URL can come from ConfigService. With no stores option you get an in-memory store inside the current process.

Reading and writing

The injected object exposes get, set, del and clear, the bulk forms mget, mset and mdel, and wrap. On cache-manager 7 get resolves to undefined on a miss; cache-manager 6 resolved to null instead, so a codebase still on 6 has to treat both as a miss. wrap(key, fn) is shorthand for get, then call, then set — the same three steps, written once.

Choosing keys

You can only delete a key you can name. Build keys from the identity of the data, like product:42 and product:42:reviews, so the code that writes a product knows exactly which strings to remove.

Example

Example · typescript
// src/products/products.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import type { Cache } from 'cache-manager';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './product.entity';

@Injectable()
export class ProductsService {
  constructor(
    // CACHE_MANAGER is a string token, so it needs an explicit @Inject
    @Inject(CACHE_MANAGER) private readonly cache: Cache,
    @InjectRepository(Product)
    private readonly products: Repository<Product>,
  ) {}

  async findOne(id: number): Promise<Product | null> {
    // wrap: return the cached value, or run the function and cache what it returns.
    // 60000 is the ttl in MILLISECONDS (one minute).
    return this.cache.wrap(
      `product:${id}`,
      () => this.products.findOneBy({ id }),
      60_000,
    );
  }

  async findSlug(id: number): Promise<string | undefined> {
    // get<T>() resolves to undefined when the key is absent or expired
    const cached = await this.cache.get<string>(`product:${id}:slug`);
    if (cached !== undefined) {
      return cached;
    }
    const product = await this.products.findOneBy({ id });
    if (!product) {
      return undefined;
    }
    await this.cache.set(`product:${id}:slug`, product.slug, 60_000);
    return product.slug;
  }
}

When to use it

  • Keep a dashboard aggregate that takes two seconds to compute out of the request path.
  • Memoise a third-party API response for shorter than that API's rate-limit window.
  • Cache a product page's data under product:<id> so the write path can delete exactly that key.
  • Hold a rarely-changing settings row so it is not read from the database on every request.

More examples

register() and registerAsync()

register() takes plain options. The async form below reads them at startup instead. Reach for registerAsync() as soon as a value belongs in the environment rather than in the source, and for isGlobal when the cache is used by more modules than it is worth importing into.

Example · typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),

    // Static options, known at build time.
    // isGlobal: true means every module can inject CACHE_MANAGER
    // without importing CacheModule itself.
    CacheModule.register({
      isGlobal: true,
      ttl: 5_000, // default time to live, in milliseconds
    }),
  ],
})
export class AppModule {}

The same options, resolved at startup

registerAsync() accepts useFactory with inject, or useClass/useExisting pointing at a class that implements CacheOptionsFactory and its createCacheOptions() method. The factory runs during bootstrap, so the cache is configured before the first request arrives.

Example · typescript
// src/cache/cache.config.module.ts
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    CacheModule.registerAsync({
      isGlobal: true,
      imports: [ConfigModule],
      // useFactory runs once, during bootstrap, before any request
      useFactory: (config: ConfigService) => ({
        ttl: config.get<number>('CACHE_TTL_MS', 30_000),
      }),
      inject: [ConfigService],
    }),
  ],
})
export class CacheConfigModule {}

Two ways to inject the cache

Both constructors receive the identical object. The short form is easier to read; the long form is what older code and most of the documentation uses, so you need to recognise it. Keep one style per project.

Example · typescript
// src/reports/reports.service.ts
import { Inject, Injectable } from '@nestjs/common';
// Cache here is a CLASS exported by @nestjs/cache-manager, so it is a usable
// token on its own. CacheModule registers it as { provide: Cache, useExisting: CACHE_MANAGER }.
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
import type { Cache as CacheType } from 'cache-manager';

@Injectable()
export class ReportsService {
  // Shorthand: the class token is read from the parameter's type metadata.
  // Needs emitDecoratorMetadata, which the Nest CLI tsconfig sets.
  constructor(private readonly cache: Cache) {}
}

@Injectable()
export class ReportsServiceLonghand {
  // Longhand: the same provider, named by its string token.
  // A string token carries no type, hence the separate type import.
  constructor(@Inject(CACHE_MANAGER) private readonly cache: CacheType) {}
}

Discussion

  • Be the first to comment on this lesson.