DEV Community

Vignesh Pugazhendhi
Vignesh Pugazhendhi

Posted on

Nestjs Series- Interceptors

Introduction

Interceptors are classes decorated with @Injectable() decorator. The core concept of interceptors is based on Aspect Oriented Programming (AOP) paradigm. AOP is a programming paradigm to that aims to increase the modularity by allowing the separation of cross-cutting concerns.

Interceptors are useful in the following scenarios:

  1. to bind some logic before a method handler is called
  2. to bind some logic after a method handler returns a response
  3. transform the exception thrown from a handler
  4. extend the basic function behaviour

Each interceptor class has to implement the NestInterceptor interface and thus have to satisfy all the method contracts.
intercept() is such a method, taking 2 arguments. First argument is ExecutionContext, which we have already discussed in nestjs pipes and guards. ExecutionContext inherits the ArgumentsHost which is a wrapper around the arguments that have been passed to the handler. By inheriting the ArgumentsHost, it has several methods to provide details about the current execution context. The second argument to intercept() is of type CallHandler. CallHandler inherits the handle() method which is used to call the route handler method at any point of the execution. This is called the Pointcut, where in additional or transformed information is being passed to the route handler method.

This means that the interceptor kind of wraps the request/response stream and thus can manipulate some logic before and after the route handler method calls. The handle() method returns an Observable, so we can use operators from rxjs to collect the response stream.

The below code snippet is taken from the official nestjs docs to cover the basic understanding of interceptors.

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

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<any> | Promise<Observable<any>> {
    console.log('Before...');
    const now = Date.now();
    return next
      .handle()
      .pipe(tap(() => console.log(`After...${Date.now() - now}`)));
  }
}

Enter fullscreen mode Exit fullscreen mode

Following code snippet is used to get the request object and perform some operations on it:

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

@Injectable()
export class DemoInterceptor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<any> | Promise<Observable<any>> {
    const ctx = context.switchToHttp();
    const requestBody = ctx.getRequest()!.body as {
      name: string;
      age: number;
    };
    if (requestBody.name.length <= 7)
      throw new BadRequestException('name should be atleast 8 characters long');
    requestBody.name = requestBody.name.toUpperCase();
    return next
      .handle()
      .pipe(tap(() => console.log('response from the method handler')));
  }
}

Enter fullscreen mode Exit fullscreen mode

Above code is very obvious. tap() operator of rxjs library is used to execute an anonymous function once the entire response stream is captured from the method handler().

Binding Interceptors

As with Guards and Pipes, Interceptors can be binded at one of the following three levels:

  1. at method handler level
  2. at module level
  3. at global level

All you have to do is to decorator the levels with @UseInterceptors() decorator and pass on the Interceptor class or an instance of it as shown in the code below:

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post('demoInterceptor')
  @UseInterceptors(new DemoInterceptor())
  async demoInterceptor(
    @Body() userDto: { name: string; age: number },
  ): Promise<any> {
    return this.userService.createUser(userDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

At the global level:

const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
Enter fullscreen mode Exit fullscreen mode

And at the module level:

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Interceptors can be used to timeout a request handler manually. When your endpoint doesn't return anything after a period of time, you want to terminate with an error response. The following construction enables this:

mport { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(5000),
      catchError(err => {
        if (err instanceof TimeoutError) {
          return throwError(new RequestTimeoutException());
        }
        return throwError(err);
      }),
    );
  };
};
Enter fullscreen mode Exit fullscreen mode

Top comments (0)