Interceptors: Transforming and Logging Responses in NestJS

In NestJS, interceptors are used to modify the response returned by a route handler. They allow you to execute additional logic before the final response is sent back to the client. Interceptors are ideal for tasks such as transforming response data, logging, caching, or adding custom headers to the response.

In this module, we will explore how to create and use interceptors in NestJS to transform and log responses.

Table of Contents

  1. Introduction
  2. What are Interceptors in NestJS?
  3. Creating Custom Interceptors
  4. Using Interceptors to Transform Responses
  5. Logging Responses with Interceptors
  6. Applying Interceptors Globally and Locally
  7. Best Practices for Interceptors
  8. Conclusion

Introduction

Interceptors are a key feature in NestJS for handling the request-response cycle. While middleware handles requests before reaching the route handler, interceptors operate after the route handler has processed the request but before the response is sent to the client. This allows you to perform various operations, such as transforming or logging the response.

In this module, we’ll dive into creating and using interceptors to transform and log responses. We’ll also cover how to apply interceptors globally and locally in your NestJS application.

What are Interceptors in NestJS?

In NestJS, interceptors are classes that implement the NestInterceptor interface. They allow you to manipulate or modify the incoming response before it’s sent to the client. Interceptors are especially useful for:

  • Transforming the response data (e.g., modifying the structure or format).
  • Logging the response data for debugging or auditing purposes.
  • Enhancing performance (e.g., caching the response).
  • Error handling in a centralized manner.

Interceptors are executed after the route handler has processed the request, but before the response is returned. You can use interceptors to modify the response body, add headers, or perform any other transformations.

Characteristics of Interceptors:

  • Interceptors can modify the request, response, or both.
  • They can be used for transformation, logging, caching, and error handling.
  • Interceptors can be applied globally or locally.
  • They are executed in the order they are defined.

Creating Custom Interceptors

To create a custom interceptor, you need to implement the NestInterceptor interface and define the intercept() method. The intercept() method takes two parameters:

  1. context: The execution context, which provides access to the request and response.
  2. next: The function that passes control to the next interceptor or handler.

Here’s an example of a basic interceptor that logs the response time for each request:

Step 1: Create a Logging Interceptor

typescriptCopyEditimport { 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> {
    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`Response time: ${Date.now() - now}ms`)),
      );
  }
}

In this example:

  • The LoggingInterceptor logs the time taken for each request to be processed.
  • We use the tap operator from RxJS to log the response time after the request is processed.

Step 2: Register the Interceptor

To use this interceptor, you must register it in a module. You can apply the interceptor globally or locally.

Globally:

In main.ts, you can apply the interceptor globally to all routes:

typescriptCopyEditimport { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';
import { Reflector } from '@nestjs/core';
import { APP_INTERCEPTOR } from '@nestjs/core';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new LoggingInterceptor());
  await app.listen(3000);
}

bootstrap();

Locally:

You can apply the interceptor to specific controllers or routes using the @UseInterceptors() decorator:

typescriptCopyEditimport { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';

@Controller('users')
export class UserController {
  @Get()
  @UseInterceptors(LoggingInterceptor)
  findAll() {
    return ['User1', 'User2', 'User3'];
  }
}

Using Interceptors to Transform Responses

One of the most common uses of interceptors is to transform the response before it reaches the client. You can modify the response data structure or apply any transformation logic you need.

Here’s an example of a transformation interceptor that adds a timestamp property to the response:

Step 1: Create a Transformation Interceptor

typescriptCopyEditimport { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next
      .handle()
      .pipe(
        map((data) => ({
          data,
          timestamp: new Date().toISOString(),
        })),
      );
  }
}

In this example:

  • The TransformInterceptor modifies the response by wrapping it in an object that includes the original data and a timestamp.

Step 2: Apply the Transformation Interceptor

You can apply the TransformInterceptor globally or locally as demonstrated earlier. Here’s how to apply it locally in a controller method:

typescriptCopyEdit@Controller('posts')
export class PostController {
  @Get()
  @UseInterceptors(TransformInterceptor)
  findAll() {
    return { title: 'NestJS Interceptors', content: 'Learn how to use interceptors' };
  }
}

This will ensure that every response from the findAll method is transformed to include the timestamp.

Logging Responses with Interceptors

Logging is a common use case for interceptors. You can log details such as the response body, status code, or any other relevant information before the response is sent to the client.

Here’s an example of a logging interceptor that logs the response data:

Step 1: Create a Logging Interceptor

typescriptCopyEditimport { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class ResponseLoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      tap((response) => {
        console.log('Response Data:', response);
      }),
    );
  }
}

In this example:

  • The ResponseLoggingInterceptor logs the response data before it is sent to the client.

Step 2: Apply the Logging Interceptor

typescriptCopyEdit@Controller('posts')
export class PostController {
  @Get()
  @UseInterceptors(ResponseLoggingInterceptor)
  findAll() {
    return { title: 'NestJS Interceptors', content: 'Learn how to use interceptors' };
  }
}

This will log the response data every time the findAll method is called.

Applying Interceptors Globally and Locally

As with middleware, you can apply interceptors globally or locally in your NestJS application.

Globally:

To apply an interceptor globally, use the app.useGlobalInterceptors() method in main.ts:

typescriptCopyEditimport { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TransformInterceptor } from './transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new TransformInterceptor());
  await app.listen(3000);
}

bootstrap();

Locally:

To apply an interceptor locally to specific controllers or routes, use the @UseInterceptors() decorator:

typescriptCopyEdit@Controller('posts')
export class PostController {
  @Get()
  @UseInterceptors(TransformInterceptor)
  findAll() {
    return { title: 'NestJS Interceptors', content: 'Learn how to use interceptors' };
  }
}

Best Practices for Interceptors

  1. Keep Interceptors Focused: An interceptor should be focused on a single concern, such as logging, transforming, or caching.
  2. Use for Cross-Cutting Concerns: Interceptors are perfect for handling tasks that apply to multiple routes, such as logging, transforming data, or managing headers.
  3. Avoid Complex Logic in Interceptors: Interceptors should not contain business logic. They should be lightweight and focused on modifying the request/response or performing non-business tasks.
  4. Chain Multiple Interceptors: You can chain multiple interceptors to perform several tasks sequentially. Ensure the order of execution is correct.

Conclusion

Interceptors in NestJS offer a powerful way to transform and log responses, making them ideal for tasks like data transformation, logging, or even error handling. By using interceptors effectively, you can maintain cleaner, more maintainable code and handle cross-cutting concerns efficiently.

Now that you understand how to create and use interceptors for transforming and logging responses, you can implement them in your NestJS applications to streamline your response handling and ensure a more robust architecture.