LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
Lesson 1018 min read

Interceptors & the Request Lifecycle

Wrap NestJS route handlers with interceptors to log timing, transform responses and hide sensitive fields, and learn the full request lifecycle order.

On this page 12 sections

Introduction

Interceptors let you run logic both before and after a route handler, and even change what the handler returns. They are perfect for cross-cutting concerns such as measuring response time, wrapping every response in a standard envelope, or removing sensitive fields like passwords.

What You Will Learn
  • How interceptors work with RxJS Observables.
  • How to measure handler execution time.
  • How to reshape every response consistently.
  • How to hide sensitive fields with ClassSerializerInterceptor.
  • The complete order of middleware, guards, interceptors, pipes and filters.

What is an Interceptor?

An interceptor implements NestInterceptor and its intercept(context, next) method. next.handle() calls the route handler and returns an RxJS Observable of its result. Code before next.handle() runs before the handler; RxJS operators piped onto the Observable run after it.

Just Enough RxJS

You only need two RxJS operators for most interceptors: tap() to run a side effect (like logging) without changing the value, and map() to replace the value with a transformed one.

A Timing Interceptor

// src/common/interceptors/timing.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class TimingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const handler = `${context.getClass().name}.${context.getHandler().name}`;
const start = Date.now();
return next
.handle()
.pipe(tap(() => console.log(`${handler} took ${Date.now() - start}ms`)));
}
}
Console

Click Run to see what this code prints.

Notice the interceptor knows exactly which controller class and method ran — something middleware cannot tell you.

Transforming Responses

Many teams want every successful response in the same envelope. An interceptor with map() does this for the whole API at once:

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { map, Observable } from 'rxjs';
export interface ApiResponse<T> {
success: true;
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<ApiResponse<T>> {
return next.handle().pipe(map((data) => ({ success: true, data })));
}
}
GET /products/1

Click Run to see what this code prints.

Hiding Fields with Serialization

Returning a user object straight from the database risks leaking its password hash. Nest's built-in ClassSerializerInterceptor uses class-transformer to apply @Exclude() and @Expose() rules on the way out — as long as you return class instances, not plain objects:

// src/users/entities/user.entity.ts
import { Exclude } from 'class-transformer';
export class UserEntity {
id: number;
email: string;
@Exclude()
passwordHash: string;
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial);
}
}
// src/users/users.controller.ts
@UseInterceptors(ClassSerializerInterceptor)
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return new UserEntity(await this.usersService.findOne(id));
}
GET /users/1

Click Run to see what this code prints.

Binding Interceptors

// route or controller level
@UseInterceptors(TimingInterceptor)
// globally, without DI (main.ts)
app.useGlobalInterceptors(new TimingInterceptor());
// globally, with DI (any module's providers)
{ provide: APP_INTERCEPTOR, useClass: TimingInterceptor }

The Full Request Lifecycle

You have now met every piece of Nest's request pipeline. Here is the order in which they run for a single request:

Incoming request
↓
Middleware (global, then module-bound)
↓
Guards (global → controller → route)
↓
Interceptors — before (global → controller → route)
↓
Pipes (global → controller → route → parameter)
↓
Route handler (controller method → services)
↓
Interceptors — after (in reverse order)
↓
Exception filters (only if an exception was thrown)
↓
Response sent to client

A quick way to remember it: middleware prepares the request, guards decide if it may continue, interceptors wrap the handler, pipes clean the arguments, the handler does the work, and filters catch anything that goes wrong along the way.

Common Mistakes

Avoid These Mistakes
  • Forgetting to return next.handle() — the handler's response is lost and the request hangs.
  • Returning plain objects and expecting @Exclude() to work — ClassSerializerInterceptor only applies to class instances.
  • Using an interceptor for authentication — guards are designed for allow/deny decisions.
  • Wrapping responses in an envelope that @Res()-based handlers bypass, creating inconsistent output.

Best Practices

  • Use tap() for side effects and map() for transformations.
  • Register ClassSerializerInterceptor globally and return entity class instances.
  • Keep interceptors small and focused — one concern per interceptor.
  • Register global interceptors with APP_INTERCEPTOR when they need injected services.

Frequently Asked Questions

Yes. If it returns its own Observable (for example of(cachedValue)) without calling next.handle(), the handler never runs — this is how response caching works.

Yes, with the RxJS catchError() operator — for example, to map a specific error to a different exception. For consistent error responses, exception filters are usually simpler.

Key Takeaways

  • Interceptors implement NestInterceptor and wrap next.handle(), an RxJS Observable.
  • tap() adds side effects like logging; map() transforms responses.
  • ClassSerializerInterceptor with @Exclude() hides sensitive fields.
  • Interceptors can be bound per route, per controller or globally (APP_INTERCEPTOR).
  • Lifecycle order: middleware → guards → interceptors → pipes → handler → interceptors → filters.

Summary

You now know every stage of Nest's request pipeline and when to use each one. Next, you will make your application configurable across environments — development, testing and production — with @nestjs/config.

Next Lesson →

Configuration & Environment Variables