Interceptors: Transforming Responses
Creating interceptors to transform responses before they are sent to the client, e.g., adding metadata or formatting the data.
NestJS Interceptors: Transforming Responses
Transforming Responses with Interceptors
In NestJS, interceptors are a powerful feature that allows you to intercept and modify the control flow of requests. While interceptors can be used for logging, caching, exception mapping, and more, their ability to transform responses before they are sent to the client is particularly useful. This allows you to standardize your API's output format, add metadata, handle errors gracefully, and generally decouple your controller logic from presentation concerns.
An interceptor is a class decorated with the @Injectable()
decorator that implements the NestInterceptor
interface. The key method in this interface is intercept(context: ExecutionContext, next: CallHandler): Observable<any> | Promise<Observable<any>>
. The ExecutionContext
provides information about the current request, including the handler, class, and arguments. The CallHandler
represents the next point in the execution pipeline, typically the route handler (controller method).
Interceptors use Reactive Extensions (RxJS) observables to handle asynchronous streams of data. By utilizing operators like map
, tap
, catchError
, etc., you can seamlessly modify the data returned by the controller before sending it to the client.
Detailed Explanation: Modifying the Response Body
The primary way interceptors modify the response body is through the map
operator from RxJS. The map
operator allows you to take the observable stream emitted by the next.handle()
and transform each emitted value. This transformed value then becomes the response sent to the client.
Here's a step-by-step breakdown:
- Implement the
NestInterceptor
interface. This involves creating a class with the@Injectable()
decorator and implementing theintercept
method. - Obtain the
Observable
fromnext.handle()
. This observable represents the data returned by the controller. - Use RxJS operators to transform the data. The
map
operator is the most common choice, but other operators liketap
(for side effects) andcatchError
(for error handling) can also be useful. - Return the modified
Observable
. The modified observable will be used to send the response to the client.
Practical Examples
This example demonstrates how to add metadata (like a timestamp and version) to every API response.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
data: T;
metadata: {
timestamp: string;
version: string;
};
}
@Injectable()
export class AddMetadataInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next
.handle()
.pipe(
map(data => ({
data,
metadata: {
timestamp: new Date().toISOString(),
version: '1.0',
},
})),
);
}
}
Explanation: The AddMetadataInterceptor
intercepts the response from the controller. The map
operator wraps the original data in a new object containing the data and a metadata object. This metadata object includes the current timestamp and API version.
Usage:
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AddMetadataInterceptor } from './add-metadata.interceptor';
@Controller('users')
@UseInterceptors(AddMetadataInterceptor)
export class UsersController {
@Get()
findAll(): any[] {
return [{ id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Doe' }];
}
}
Expected Response:
{
"data": [
{
"id": 1,
"name": "John Doe"
},
{
"id": 2,
"name": "Jane Doe"
}
],
"metadata": {
"timestamp": "2023-10-27T10:00:00.000Z", // Example Timestamp
"version": "1.0"
}
}
This example demonstrates how to format a data structure before sending it to the client. For instance, you might want to transform a nested object into a flat array.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class FormatDataInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
map(data => {
if (Array.isArray(data)) {
return data.map(item => ({
userId: item.id,
userName: item.name,
userEmail: item.email
}));
} else {
return {
userId: data.id,
userName: data.name,
userEmail: data.email
};
}
}),
);
}
}
Explanation: The FormatDataInterceptor
checks if the returned data is an array. If it is, it iterates over the array and transforms each object to have a different structure (renaming properties). If the data is not an array, it is considered a single object that should be transformed.
Usage:
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { FormatDataInterceptor } from './format-data.interceptor';
@Controller('users')
@UseInterceptors(FormatDataInterceptor)
export class UsersController {
@Get()
findAll(): any[] {
return [{ id: 1, name: 'John Doe', email: 'john.doe@example.com' }, { id: 2, name: 'Jane Doe', email: 'jane.doe@example.com' }];
}
}
Expected Response:
[
{
"userId": 1,
"userName": "John Doe",
"userEmail": "john.doe@example.com"
},
{
"userId": 2,
"userName": "Jane Doe",
"userEmail": "jane.doe@example.com"
}
]
Interceptors can also be used to catch errors and transform them into a more user-friendly format. This is achieved using the catchError
operator.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, HttpException, HttpStatus } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorHandlingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => {
console.error(err); // Log the error for debugging
return throwError(() => new HttpException('An unexpected error occurred.', HttpStatus.INTERNAL_SERVER_ERROR));
}),
);
}
}
Explanation: The ErrorHandlingInterceptor
intercepts any errors thrown by the controller. The catchError
operator catches the error and transforms it into a more generic HttpException
. The original error is logged for debugging purposes.
Usage:
import { Controller, Get, UseInterceptors, HttpException, HttpStatus } from '@nestjs/common';
import { ErrorHandlingInterceptor } from './error-handling.interceptor';
@Controller('users')
@UseInterceptors(ErrorHandlingInterceptor)
export class UsersController {
@Get()
findAll(): any[] {
throw new HttpException('Failed to fetch users', HttpStatus.BAD_REQUEST);
}
}
Expected Response (when an error occurs):
{
"statusCode": 500,
"message": "An unexpected error occurred.",
"error": "Internal Server Error"
}