This lesson builds a complete, practical custom interceptor: a simple in-memory response cache, showing how to combine ExecutionContext, RxJS operators, and conditional logic inside intercept().
A Simple Caching Interceptor
The interceptor checks an in-memory cache before calling the handler. If a cached value exists, it returns it immediately (skipping next.handle() entirely); otherwise, it calls the handler and caches the result for next time.
of(this.cache.get(key)) returns an Observable that immediately emits the cached value, entirely bypassing the actual route handler on a cache hit.
Applying Only to GET Requests
intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest();
if (request.method !== 'GET') {
return next.handle();
}
// caching logic only for GET
}
Checking request.method lets an interceptor apply conditional logic based on the HTTP verb.
of(value) (from rxjs) wraps a plain value in an Observable when you need to skip next.handle().
A real-world cache interceptor would use TTLs and a shared store (like Redis) instead of an in-memory Map.
Cache keys typically need to include query parameters and, for per-user data, the authenticated user's id.
Custom Interceptor Building Blocks
Pieces used to build a caching (or similar) interceptor.
Piece
Purpose
context.switchToHttp().getRequest()
Access the current request for cache key logic
of(value)
Emit a value immediately without calling the handler
next.handle().pipe(tap(...))
Run a handler and store its result afterward
CACHE_TTL metadata
Optional per-route cache duration via a custom decorator
Cache Invalidation Matters More Than Caching
The hardest part of any caching layer isn't storing values, it's knowing when to invalidate them. A POST, PATCH, or DELETE on the same resource should typically clear the matching cache entries, otherwise clients see stale data indefinitely.
Invalidate (or don't cache) mutating requests entirely, only cache safe GET requests.
Clear related cache keys whenever the underlying resource changes.
Prefer short TTLs over indefinite caching unless invalidation is airtight.
Nest's Built-in CacheInterceptor
NestJS ships its own CacheInterceptor (from @nestjs/cache-manager) with pluggable stores, including Redis, for production use. Building a custom one, as in this lesson, is valuable for learning the mechanics before adopting the official module.
Common Mistakes
Caching responses that include per-user data using a cache key that doesn't account for the user.
Never invalidating cached entries after a mutating request changes the underlying data.
Using an in-memory cache in a multi-instance deployment, where each instance has its own inconsistent cache.
Caching error responses, causing a temporary failure to be served repeatedly from cache.
Key Takeaways
A custom interceptor can short-circuit next.handle() entirely by returning of(value).
tap() is useful for storing a handler's result as a side effect without altering it.
Cache invalidation strategy matters more than the caching mechanism itself.
NestJS's official CacheInterceptor and @nestjs/cache-manager are the production-ready path for real caching.
Pro Tip
Before building a custom cache interceptor for production use, check whether @nestjs/cache-manager's built-in CacheInterceptor combined with a Redis store already covers your needs, most caching requirements don't need a fully custom implementation.
You now know how to build a custom interceptor. Next, learn about Exception Filters, which control how errors are turned into responses.