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:

  1. Implement the NestInterceptor interface. This involves creating a class with the @Injectable() decorator and implementing the intercept method.
  2. Obtain the Observable from next.handle(). This observable represents the data returned by the controller.
  3. Use RxJS operators to transform the data. The map operator is the most common choice, but other operators like tap (for side effects) and catchError (for error handling) can also be useful.
  4. Return the modified Observable. The modified observable will be used to send the response to the client.

Practical Examples

Adding Metadata to the Response

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"
  }
} 
Formatting Data Structures

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"
  }
] 
Handling Errors Gracefully

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"
}