Skip to content
NestJS ns caching 4 min read

Automatic Response Caching

Reading from the cache by hand with CACHE_MANAGER is precise but repetitive: every read-heavy handler ends up with the same “check the cache, miss, compute, store” dance. NestJS ships a CacheInterceptor that automates this entirely for HTTP GET requests — it derives a cache key from the request URL, returns the cached response on a hit, and transparently stores the handler’s result on a miss. You can apply it to a single route, a whole controller, or the entire application, and fine-tune the key and TTL per route with the @CacheKey and @CacheTTL decorators.

How the interceptor works

CacheInterceptor wraps your route handlers. Before a handler runs it computes a cache key (by default the request URL, e.g. /products?page=2) and looks it up in the configured store. On a hit, the cached value is returned immediately and the handler is never invoked. On a miss, the handler executes normally and its return value is written to the cache under that key, governed by the module’s ttl.

Crucially, the interceptor only caches GET requests. Mutating verbs like POST, PUT, PATCH, and DELETE are ignored, which keeps you from accidentally serving a stale response to a write. It also respects per-route metadata, so you can override the key and TTL without writing imperative code.

Caching a single route

The simplest scope is a single handler. Apply CacheInterceptor with the @UseInterceptors decorator on the method you want cached. This assumes you have already registered CacheModule (see the caching overview).

// src/products/products.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { CacheInterceptor } from '@nestjs/cache-manager';
import { ProductsService } from './products.service';

@Controller('products')
export class ProductsController {
  constructor(private readonly products: ProductsService) {}

  @UseInterceptors(CacheInterceptor)
  @Get('catalog')
  findCatalog() {
    return this.products.findCatalog(); // slow aggregation
  }
}

The first request to /products/catalog runs the handler and caches the result; subsequent requests within the TTL window return instantly.

Output:

GET /products/catalog  -> 287ms  (cache miss, handler ran)
GET /products/catalog  -> 2ms    (cache hit, handler skipped)
GET /products/catalog  -> 1ms    (cache hit, handler skipped)

Caching a whole controller

Decorate the controller class instead of a method to cache every GET it exposes. Each route is keyed by its own URL, so they never collide.

// src/articles/articles.controller.ts
import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { CacheInterceptor } from '@nestjs/cache-manager';
import { ArticlesService } from './articles.service';

@UseInterceptors(CacheInterceptor)
@Controller('articles')
export class ArticlesController {
  constructor(private readonly articles: ArticlesService) {}

  @Get()
  findAll() {
    return this.articles.findAll();
  }

  @Get(':slug')
  findOne(@Param('slug') slug: string) {
    return this.articles.findOne(slug);
  }
}

Caching globally

To cache reads across the whole application, register CacheInterceptor as a global interceptor using the APP_INTERCEPTOR token. This binds it through the DI container so it can resolve the cache provider.

// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { CacheModule, CacheInterceptor } from '@nestjs/cache-manager';

@Module({
  imports: [CacheModule.register({ isGlobal: true, ttl: 30_000 })],
  providers: [{ provide: APP_INTERCEPTOR, useClass: CacheInterceptor }],
})
export class AppModule {}

Binding CacheInterceptor globally caches every GET route by default, including ones that return user-specific data. Audit your endpoints first, and exclude per-user or always-fresh routes explicitly (see below).

Customizing key and TTL

The URL-based key is fine for static endpoints, but for routes whose meaningful cache identity differs from the path you can override it. Use @CacheKey to set an explicit key and @CacheTTL to set a per-route lifetime in milliseconds.

// src/stats/stats.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager';
import { StatsService } from './stats.service';

@UseInterceptors(CacheInterceptor)
@Controller('stats')
export class StatsController {
  constructor(private readonly stats: StatsService) {}

  @CacheKey('daily-stats')
  @CacheTTL(60_000) // cache for 60 seconds, overriding the module default
  @Get('daily')
  getDaily() {
    return this.stats.computeDaily();
  }
}
DecoratorArgumentPurpose
@CacheKey('key')stringReplaces the auto-generated URL key with a fixed name
@CacheTTL(ms)numberOverrides the module ttl for this route, in milliseconds
@UseInterceptors(CacheInterceptor)classActivates auto-caching for a route or controller

Excluding routes from caching

Sometimes a GET route must never be cached — a live status check, a per-user feed, or an endpoint with side effects. The cleanest approach is to not bind the interceptor to those routes. When you cache globally, subclass CacheInterceptor and override isRequestCacheable to skip selected paths.

// src/cache/http-cache.interceptor.ts
import { CacheInterceptor } from '@nestjs/cache-manager';
import { ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class HttpCacheInterceptor extends CacheInterceptor {
  private readonly excluded = ['/health', '/me'];

  isRequestCacheable(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest<{ method: string; path: string }>();
    if (this.excluded.some((path) => request.path.startsWith(path))) {
      return false;
    }
    return super.isRequestCacheable(context);
  }
}

Register HttpCacheInterceptor via APP_INTERCEPTOR in place of the built-in class, and the listed paths bypass the cache while everything else is cached as usual.

Best Practices

  • Reserve auto-caching for genuinely idempotent, read-heavy GET routes; never let it touch endpoints that depend on the authenticated user.
  • Prefer the per-route @UseInterceptors(CacheInterceptor) scope over a global binding unless most of your API is safe to cache.
  • When caching globally, subclass the interceptor and override isRequestCacheable to exclude health checks, per-user feeds, and side-effecting reads.
  • Use @CacheTTL to give volatile data a short lifetime and stable data a long one — remember the value is in milliseconds.
  • Set @CacheKey only when the URL is a poor identity for the response; otherwise the automatic URL key is simpler and collision-free.
  • Pair auto-caching with imperative del/set calls on write paths so a mutation invalidates the cached read it affects.
Last updated June 14, 2026
Was this helpful?