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: Global and Local

Introduction to Interceptors in NestJS

Interceptors in NestJS are a powerful feature that allows you to intercept and transform requests and responses. They can be used for a variety of purposes, including logging, caching, exception handling, and request validation. They are similar to middleware, but offer greater flexibility and control over the execution pipeline within the controller.

Applying Interceptors Globally

Applying interceptors globally means they will be executed for every route in your NestJS application. This is useful for tasks like global logging, setting default headers, or global exception handling.

How to apply an interceptor globally:

  1. Create an Interceptor: Define your interceptor class, implementing the NestInterceptor interface and the intercept() method.
  2. Register the Interceptor in the Module: Use the APP_INTERCEPTOR provider in your application module (usually AppModule) to register the interceptor globally.

Example:

1. Create a Simple Logging Interceptor

// src/interceptors/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 request = context.switchToHttp().getRequest();
        this.logger.log(`Incoming request: ${request.method} ${request.url}`);


        return next
          .handle()
          .pipe(
            tap(() => {
              this.logger.log(`Request completed in ${Date.now() - now}ms`);
            }),
          );
      }
    }

2. Register the Interceptor Globally in AppModule

// src/app.module.ts
    import { Module } from '@nestjs/common';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { APP_INTERCEPTOR } from '@nestjs/core';
    import { LoggingInterceptor } from './interceptors/logging.interceptor';

    @Module({
      imports: [],
      controllers: [AppController],
      providers: [
        AppService,
        {
          provide: APP_INTERCEPTOR,
          useClass: LoggingInterceptor,
        },
      ],
    })
    export class AppModule {}

Now, the LoggingInterceptor will be executed for every request that comes into your application.

Applying Interceptors Locally

Applying interceptors locally means they are only executed for specific controllers or routes. This provides more granular control over which requests are intercepted.

How to apply an interceptor locally:

  1. Use the @UseInterceptors() decorator: You can apply interceptors to specific controllers or routes using the @UseInterceptors() decorator.

Example:

// src/controllers/example.controller.ts
    import { Controller, Get, UseInterceptors, Param } from '@nestjs/common';
    import { LoggingInterceptor } from '../interceptors/logging.interceptor';
    import { TransformInterceptor } from '../interceptors/transform.interceptor';

    @Controller('example')
    @UseInterceptors(LoggingInterceptor) // Applied to all routes in this controller
    export class ExampleController {

      @Get()
      getExample(): string {
        return 'This is an example endpoint.';
      }

      @Get(':id')
      @UseInterceptors(TransformInterceptor) // Applied only to this route
      getExampleById(@Param('id') id: string): string {
        return `Example ID: ${id}`;
      }
    }

In this example, LoggingInterceptor is applied to all routes within the ExampleController, while TransformInterceptor is applied only to the getExampleById route.

Order of Execution of Interceptors

When multiple interceptors are applied, their order of execution is crucial.

Order:

  1. Global Interceptors: Applied in the order they are defined in the AppModule (or any other module where APP_INTERCEPTOR is used).
  2. Controller-Level Interceptors: Applied in the order they are defined using the @UseInterceptors() decorator on the controller.
  3. Route-Level Interceptors: Applied in the order they are defined using the @UseInterceptors() decorator on the route.

For example, if you have a global interceptor A, a controller-level interceptor B, and a route-level interceptor C, the execution order will be A -> B -> C before the request reaches the handler, and C -> B -> A after the handler returns a result. This follows a nested structure.

Example:

Let's say you have the following setup:

  • Global Interceptor: GlobalInterceptor
  • Controller-Level Interceptor: ControllerInterceptor
  • Route-Level Interceptor: RouteInterceptor

When a request hits a specific route within the controller, the order of execution would be:

  1. GlobalInterceptor (Before the request reaches the controller)
  2. ControllerInterceptor (Before the request reaches the handler)
  3. RouteInterceptor (Before the request reaches the handler)
  4. Handler (The route handler in the controller executes)
  5. RouteInterceptor (After the handler returns a result)
  6. ControllerInterceptor (After the handler returns a result)
  7. GlobalInterceptor (After the handler returns a result)

This nested execution order allows you to chain interceptors to perform complex transformations and operations on the request and response.

Important Considerations

  • Exception Handling: Interceptors can be used to catch and handle exceptions thrown by route handlers.
  • Performance: Be mindful of the performance impact of adding multiple interceptors, especially global ones.
  • Dependency Injection: Interceptors can inject dependencies like other services or repositories.