Interceptors: Transforming Responses

Creating interceptors to transform responses before they are sent to the client, e.g., adding metadata or formatting the data.


NestJS Interceptor Scope and Context

What are Interceptors in NestJS?

Interceptors in NestJS are a powerful feature that allows you to intercept and transform the request and response of route handlers. They are used to add extra logic before or after a handler is executed, such as logging, caching, exception mapping, or data serialization. They are similar to aspect-oriented programming (AOP) concepts.

Interceptor Scope and Context

Scope

The scope of an interceptor in NestJS determines where it's applied. Interceptors can be defined at three different levels:

  • Global: Applied to every route handler in the application.
  • Controller: Applied to all route handlers within a specific controller.
  • Route Handler: Applied to a specific route handler (a single method within a controller).

You apply interceptors using the @UseInterceptors() decorator. Global interceptors are registered using app.useGlobalInterceptors() in your main.ts or main.js file.

Example:

// Applying an interceptor globally (main.ts)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './interceptors/logging.interceptor';

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

// Applying an interceptor to a controller (example.controller.ts)
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { ExampleInterceptor } from './interceptors/example.interceptor';

@Controller('example')
@UseInterceptors(ExampleInterceptor) // Applied to all handlers in this controller
export class ExampleController {
  @Get()
  getHello(): string {
    return 'Hello World!';
  }
}

// Applying an interceptor to a route handler (specific method)
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { SpecificInterceptor } from './interceptors/specific.interceptor';

@Controller('another-example')
export class AnotherExampleController {
  @Get()
  @UseInterceptors(SpecificInterceptor) // Applied only to this handler
  getAnotherHello(): string {
    return 'Another Hello!';
  }
} 

Execution Context

The execution context provides access to important information about the current request lifecycle within the interceptor. It provides access to the following:

  • Handler Method: Allows you to get the reflected handler (the actual method being executed).
  • Class Context: Allows you to get the class the handler is defined in (the controller).
  • ExecutionContext API: Provides methods like getType() (e.g., 'http', 'rpc', 'ws'), switchToHttp() (for HTTP-specific context), switchToRpc() (for gRPC context), and switchToWs() (for WebSocket context).

You access the execution context through the ExecutionContext object provided to the interceptor's intercept() method. Usually we'll use the HTTP context by using context.switchToHttp()

Example:

// Example of accessing ExecutionContext (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 httpContext = context.switchToHttp();
    const request = httpContext.getRequest();
    const method = request.method;
    const url = request.url;

    return next
      .handle()
      .pipe(
        tap(() => {
          this.logger.log(`Method: ${method}; URL: ${url}; Execution time: ${Date.now() - now}ms`);
        }),
      );
  }
} 

Dependency Injection within Interceptors

Interceptors are a part of the NestJS dependency injection system. This means you can inject services and other dependencies into your interceptors using the @Injectable() decorator and the constructor.

Example:

// Example of Dependency Injection (cache.interceptor.ts)
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Inject } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const key = `cache:${context.getClass().name}:${context.getHandler().name}`;
    const cachedValue = await this.cacheManager.get(key);

    if (cachedValue) {
      return of(cachedValue); // Return cached value if it exists
    }

    return next
      .handle()
      .pipe(
        tap(async (value) => {
          await this.cacheManager.set(key, value); // Cache the result
        }),
      );
  }
} 

Access to Request Lifecycle Information

As demonstrated in the examples above, interceptors have access to crucial information about the request lifecycle through the ExecutionContext. This includes:

  • Request Object: The raw request object (e.g., request.body, request.params, request.headers in HTTP contexts).
  • Response Object: The response object (for modifying the response).
  • Handler Method: The route handler method that will be executed.
  • Class Context: The controller class that contains the handler method.

This level of access allows interceptors to perform a wide range of tasks, from validating input to modifying output.

Key Takeaways

  • Interceptors provide a powerful way to add cross-cutting concerns to your NestJS application.
  • They can be scoped globally, to controllers, or to specific route handlers.
  • The ExecutionContext provides access to request lifecycle information.
  • Interceptors support dependency injection.