Interceptors: Transforming Responses
Creating interceptors to transform responses before they are sent to the client, e.g., adding metadata or formatting the data.
Custom Interceptors in NestJS: A Practical Guide
Creating Custom Interceptors: The Power of Interception
In NestJS, interceptors provide a powerful mechanism for modifying request and response data, handling exceptions, extending controller behavior, and much more. They act as a middleware-like layer around your route handlers, giving you fine-grained control over the request-response lifecycle. Unlike middleware which operates globally, interceptors can be applied at specific controllers, routes, or even globally, offering more targeted functionality. This guide will walk you through the process of creating and using custom interceptors in NestJS.
Building Custom Interceptors: A Step-by-Step Guide
1. Defining an Interceptor Class
First, you need to create a class that implements the NestInterceptor
interface. NestJS's dependency injection system makes this easy. This class will contain the logic you want to execute during the request-response cycle.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } 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> {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
Explanation:
@Injectable()
: Makes the interceptor injectable into other components.LoggingInterceptor implements NestInterceptor
: Declares that this class is an interceptor.intercept(context: ExecutionContext, next: CallHandler): Observable<any>
: This is the core method where the interception logic resides.context: ExecutionContext
: Provides access to the current execution context, allowing you to inspect the request, response, and other relevant data.next: CallHandler
: Represents the next handler in the execution pipeline. Callingnext.handle()
executes the route handler (controller method).Observable<any>
: Theintercept
method must return anObservable
. This allows you to asynchronously process the request and response.
next.handle()
: Invokes the route handler and returns anObservable
of the result.pipe(tap(...))
: Uses RxJS'stap
operator to execute code *after* the route handler has finished, allowing you to inspect or modify the response.
2. Implementing the intercept()
Method
The intercept()
method is where the magic happens. It receives two arguments:
ExecutionContext
: This object provides access to various information about the current execution context, including the request, response, arguments passed to the handler, and the class and method being invoked.CallHandler
: This object contains thehandle()
method, which is responsible for invoking the route handler. You must callhandle()
to execute the route handler and continue the request processing pipeline.
Accessing Request and Response Data:
The ExecutionContext
provides methods to access the underlying request and response objects. The specific methods depend on the execution context (e.g., HTTP, WebSocket, GraphQL).
For HTTP requests:
const httpContext = context.switchToHttp();
const request = httpContext.getRequest<Request>(); // Express request object
const response = httpContext.getResponse<Response>(); // Express response object
Using the request and response objects, you can access headers, body, status codes, and other relevant information.
3. Applying the Interceptor
You can apply interceptors at different levels:
- Globally: In your
main.ts
file:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
await app.listen(3000);
}
bootstrap();
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';
@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
@Get()
findAll(): string {
return 'This action returns all users';
}
}
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';
@Controller('users')
export class UsersController {
@Get()
@UseInterceptors(LoggingInterceptor)
findAll(): string {
return 'This action returns all users';
}
}
4. Example: Transforming Response Data
This example shows how to modify the response data before it is sent to the client.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(map(data => ({ data })));
}
}
This interceptor wraps the response data in an object with a data
property. For example, if the route handler returns { message: 'Hello' }
, the interceptor will transform it to { data: { message: 'Hello' } }
.
5. Handling Exceptions
Interceptors can also be used to handle exceptions. You can use RxJS's catchError
operator to catch exceptions thrown by the route handler and perform custom error handling logic.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, HttpException, HttpStatus } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(() => new HttpException('Custom error message', HttpStatus.INTERNAL_SERVER_ERROR))),
);
}
}
This interceptor catches any exceptions thrown by the route handler and re-throws them as an HttpException
with a custom error message.
Conclusion
Custom interceptors are a valuable tool in NestJS for adding cross-cutting concerns and modifying request/response behavior. By understanding how to define interceptor classes, implement the intercept()
method, and access request/response data, you can create powerful and reusable interceptors to enhance your NestJS applications.