Interceptors: Transforming Responses
Creating interceptors to transform responses before they are sent to the client, e.g., adding metadata or formatting the data.
NestJS Interceptor Scope and Context
What are Interceptors in NestJS?
Interceptors in NestJS are a powerful feature that allows you to intercept and transform the request and response of route handlers. They are used to add extra logic before or after a handler is executed, such as logging, caching, exception mapping, or data serialization. They are similar to aspect-oriented programming (AOP) concepts.
Interceptor Scope and Context
Scope
The scope of an interceptor in NestJS determines where it's applied. Interceptors can be defined at three different levels:
- Global: Applied to every route handler in the application.
- Controller: Applied to all route handlers within a specific controller.
- Route Handler: Applied to a specific route handler (a single method within a controller).
You apply interceptors using the @UseInterceptors()
decorator. Global interceptors are registered using app.useGlobalInterceptors()
in your main.ts
or main.js
file.
Example:
// Applying an interceptor globally (main.ts)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
await app.listen(3000);
}
bootstrap();
// Applying an interceptor to a controller (example.controller.ts)
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { ExampleInterceptor } from './interceptors/example.interceptor';
@Controller('example')
@UseInterceptors(ExampleInterceptor) // Applied to all handlers in this controller
export class ExampleController {
@Get()
getHello(): string {
return 'Hello World!';
}
}
// Applying an interceptor to a route handler (specific method)
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { SpecificInterceptor } from './interceptors/specific.interceptor';
@Controller('another-example')
export class AnotherExampleController {
@Get()
@UseInterceptors(SpecificInterceptor) // Applied only to this handler
getAnotherHello(): string {
return 'Another Hello!';
}
}
Execution Context
The execution context provides access to important information about the current request lifecycle within the interceptor. It provides access to the following:
- Handler Method: Allows you to get the reflected handler (the actual method being executed).
- Class Context: Allows you to get the class the handler is defined in (the controller).
ExecutionContext
API: Provides methods likegetType()
(e.g.,'http'
,'rpc'
,'ws'
),switchToHttp()
(for HTTP-specific context),switchToRpc()
(for gRPC context), andswitchToWs()
(for WebSocket context).
You access the execution context through the ExecutionContext
object provided to the interceptor's intercept()
method. Usually we'll use the HTTP context by using context.switchToHttp()
Example:
// Example of accessing ExecutionContext (logging.interceptor.ts)
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
const httpContext = context.switchToHttp();
const request = httpContext.getRequest();
const method = request.method;
const url = request.url;
return next
.handle()
.pipe(
tap(() => {
this.logger.log(`Method: ${method}; URL: ${url}; Execution time: ${Date.now() - now}ms`);
}),
);
}
}
Dependency Injection within Interceptors
Interceptors are a part of the NestJS dependency injection system. This means you can inject services and other dependencies into your interceptors using the @Injectable()
decorator and the constructor.
Example:
// Example of Dependency Injection (cache.interceptor.ts)
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Inject } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const key = `cache:${context.getClass().name}:${context.getHandler().name}`;
const cachedValue = await this.cacheManager.get(key);
if (cachedValue) {
return of(cachedValue); // Return cached value if it exists
}
return next
.handle()
.pipe(
tap(async (value) => {
await this.cacheManager.set(key, value); // Cache the result
}),
);
}
}
Access to Request Lifecycle Information
As demonstrated in the examples above, interceptors have access to crucial information about the request lifecycle through the ExecutionContext
. This includes:
- Request Object: The raw request object (e.g.,
request.body
,request.params
,request.headers
in HTTP contexts). - Response Object: The response object (for modifying the response).
- Handler Method: The route handler method that will be executed.
- Class Context: The controller class that contains the handler method.
This level of access allows interceptors to perform a wide range of tasks, from validating input to modifying output.
Key Takeaways
- Interceptors provide a powerful way to add cross-cutting concerns to your NestJS application.
- They can be scoped globally, to controllers, or to specific route handlers.
- The
ExecutionContext
provides access to request lifecycle information. - Interceptors support dependency injection.