Chapter 10 — Interceptors (AOP Magic)
Chapter 10 — Interceptors (AOP Magic)
Hey everyone! Welcome back to Namaste NestJS! 🙏
Guards decide if a request runs. Interceptors wrap logic around it — they run code before the handler and after it, on the way back out. This "before and after" power is called AOP (Aspect-Oriented Programming), and it's how you add logging, timing, response reshaping, and caching without touching your business logic. It feels like magic; let's make it obvious.
What we will cover:
- What an interceptor is and the before/after model
- The CallHandler and RxJS stream
- A logging/timing interceptor
- Transforming every response into a standard envelope
- Applying interceptors (method, controller, global)
- Where interceptors sit in the pipeline
- Real-world uses (caching, timeout, serialization)
- Common mistakes
- Interview Questions
1. What is an Interceptor?
┌─────────────────────────────────────────────────────────────┐ │ WHAT IS AN INTERCEPTOR? │ ├─────────────────────────────────────────────────────────────┤ │ │ │ A class that wraps the handler, running logic: │ │ │ │ ┌── BEFORE the handler ──┐ │ │ │ (e.g. start a timer) │ │ │ ▼ │ │ │ [ HANDLER runs ] │ │ │ │ │ │ │ ▼ │ │ │ └── AFTER the handler ────┘ │ │ (e.g. log duration, reshape the response) │ │ │ └─────────────────────────────────────────────────────────────┘
A simple analogy: an interceptor is a gift-wrapping service. Your handler produces the gift (the data); the interceptor can prep things before, then wrap and decorate the result before it's handed to the customer. The handler never knows it was wrapped.
2. The Shape of an Interceptor
An interceptor implements NestInterceptor with an intercept(context, next) method. The key is next.handle() — it returns an RxJS Observable of the handler's eventual result.
import { NestInterceptor, ExecutionContext, CallHandler, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class SampleInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// --- BEFORE the handler runs ---
console.log('Before...');
return next
.handle() // 👈 this RUNS the handler; returns a stream of its result
.pipe(
// --- AFTER the handler runs (operate on the response) ---
);
}
}
THE TWO HALVES: ──────────────── code BEFORE next.handle() → runs before the handler next.handle() → runs the handler .pipe(...) operators → run AFTER, on the returned data
Q: Do I need to master RxJS?
A: No — just two operators cover almost everything: tap() for side effects like logging (doesn't change the data), and map() for transforming the response. That's 95% of interceptor work.
3. A Logging / Timing Interceptor
The classic example — measure how long each request takes using tap():
import { NestInterceptor, ExecutionContext, CallHandler, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now(); // BEFORE: start the clock
const req = context.switchToHttp().getRequest();
return next.handle().pipe(
tap(() => { // AFTER: handler has finished
console.log(`${req.method} ${req.url} — ${Date.now() - now}ms`);
}),
);
}
}
HAND TRACE: GET /cats (with LoggingInterceptor)
────────────────────────────────────────────────────
1. now = 1000 (before)
2. next.handle() → runs CatsController.findAll() → returns ['Tom']
3. tap() fires: logs "GET /cats — 12ms"
4. response ['Tom'] passes through unchanged
▼
Client gets ['Tom'], and you logged the timing ✔
4. Transforming Every Response
Want every endpoint to return a consistent envelope like { data, timestamp }? Use map() — no need to change a single handler.
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => ({ // 👈 wrap whatever the handler returned
data,
timestamp: new Date().toISOString(),
})),
);
}
}
BEFORE (raw handler return): ["Tom", "Garfield"]
AFTER (interceptor wraps it):
{
"data": ["Tom", "Garfield"],
"timestamp": "2026-07-06T10:00:00.000Z"
}
This is the AOP win: one interceptor standardizes the response of your entire API. Change the envelope once, everywhere updates.
5. Applying Interceptors
Same three levels you've seen for guards and pipes:
| Level | How |
|---|---|
| Method | @UseInterceptors(LoggingInterceptor) on a handler |
| Controller | @UseInterceptors(LoggingInterceptor) on the class |
| Global | app.useGlobalInterceptors(new LoggingInterceptor()) or an APP_INTERCEPTOR provider |
@Controller('cats')
@UseInterceptors(LoggingInterceptor) // log/time every route here
export class CatsController { /* ... */ }
6. Where Interceptors Sit in the Pipeline
Interceptors are unique — they run on both sides of everything else:
FULL PIPELINE (Deep Dive 02 has the complete version):
────────────────────────────────────────────────────────
Request
│
Middleware
│
Guards
│
┌─ Interceptor (BEFORE) ──────────────────┐
│ Pipes → HANDLER │
└─ Interceptor (AFTER) ───────────────────┘
│
Response
In simple words: the interceptor's "before" code runs just after guards; then pipes + the handler execute; then the interceptor's "after" code runs on the result. It literally brackets the handler.
7. Real-World Uses
COMMON INTERCEPTOR JOBS: ───────────────────────── ✔ Logging & timing → tap() ✔ Standard response shape → map() ✔ Caching → return cached Observable, skip handler ✔ Timeouts → RxJS timeout() operator ✔ Serialization → ClassSerializerInterceptor (hide @Exclude fields) ✔ Error mapping → catchError() to reshape thrown errors
Pro tip: Nest's built-in ClassSerializerInterceptor automatically strips fields you mark with @Exclude() (like a password hash) from responses — a must for entities you return directly.
8. Little Traps Beginners Fall Into
┌─────────────────────────────────────────────────────────────┐ │ COMMON MISTAKES ❌ │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Forgetting to return next.handle(). │ │ → The handler never runs; the request hangs. │ │ │ │ Using tap() when you meant to CHANGE the response. │ │ → tap() is side-effects only; use map() to transform. │ │ │ │ Putting authorization in an interceptor. │ │ → Allow/block belongs in a GUARD. │ │ │ │ Heavy blocking work in the before-phase. │ │ → Keep it light; it runs on every request. │ │ │ └─────────────────────────────────────────────────────────────┘
Interview Questions — Quick Fire!
Q: What is an interceptor in NestJS?
"An interceptor is a class implementing NestInterceptor that wraps a route handler, running logic before and after it. It's inspired by Aspect-Oriented Programming and is used for cross-cutting concerns like logging, timing, transforming responses, caching, and timeouts — without polluting the handler's own code."
Q: What does next.handle() do?
"next.handle() invokes the route handler and returns an RxJS Observable that emits the handler's result. Code before next.handle runs before the handler; operators piped onto its returned Observable, like tap or map, run after the handler on its result."
Q: What's the difference between tap and map in an interceptor?
"tap performs a side effect without altering the response — perfect for logging or metrics. map transforms the response into something new — for example wrapping every result in a standard envelope. Use tap when you only observe, map when you reshape."
Q: How is an interceptor different from a guard?
"A guard only decides whether the request may proceed and runs before the handler. An interceptor runs on both sides of the handler and can transform the request handling or the response, but it isn't meant to allow or block access. Authorization goes in guards; response shaping and logging go in interceptors."
Q: Give a real-world use of an interceptor.
"A very common one is a transform interceptor that wraps every response in a consistent structure like data and timestamp, so the whole API has a uniform shape. Another is Nest's ClassSerializerInterceptor, which strips fields marked with @Exclude, such as password hashes, from serialized responses."
Key Points to Remember
| Concept | Key Takeaway |
|---|---|
| Interceptor | Wraps the handler: runs code before AND after it (AOP). |
| next.handle() | Runs the handler; returns an Observable of the result. |
| tap vs map | tap = side effects (logging); map = transform the response. |
| Applying | @UseInterceptors at method/controller, or global. |
| Uses | Logging, timing, response envelope, caching, serialization. |
| Not for auth | Allow/block belongs in guards, not interceptors. |
What's Next?
We've covered the happy path — but what happens when something throws? A bad ID, a failed DB call, an unauthorized access. In Chapter 11 we finish Season 2 with Exception Filters: how Nest turns thrown errors into clean HTTP responses, the built-in HttpException family, and how to craft your own global error format.
Keep coding, keep shipping! See you in the next one!
Post a Comment