Node.js Framework Series — 1.2.5. NestJS — Interceptor

--

Interceptors which have useful features that are inspired by Aspect Oriented Programming (AOP)[1] design pattern are classes annotated the @Injectable decorator and should implement the NestInterceptor interface. We can figure the usage of interceptors as shown below.

NestJS Architecture — Interceptors

As shown above, NestJS is able to trigger the interceptor before, after method execution, when the method throws an exception, when we want to override method response depending on any conditions (e.g., caching purpose) or want to extend of method functionality.

To create an interceptor is really easy. As we said above, interceptor class should implement the NestInterceptor interface. When you look at this interface, you will see intercept method definition with parameters which are ExecutionContext object and CallHandler object. This interface forces us to implement intercept method with the right parameters.

Binding interceptors

Let’s continue to explain an interceptor with a NestJS example. We will continue to work with our property-users microservice. As you know we have several components in it. We will create one audit log as an interceptor class and see how we inject this interceptor to the controller, the provider, the module etc..

First, we will create an interceptor class as follows:

auditlog.interceptor.ts

import {
CallHandler, ExecutionContext, Injectable, NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class AuditLogInterceptor 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}ms`)));
}
}

As you see, it is really simple to create the interceptors, and also, we are able to inject this class where we want by using @Injectable decorator, because it is also provider.

So, let’s use it in a controller:

register.controller.ts

import { Controller, Body, Post, UseInterceptors } from '@nestjs/common';
import { AuditLogInterceptor } from 'src/auditlog.interceptor';
import { RegisterDto } from './dto/register.dto';
@Controller('register')
@UseInterceptors(AuditLogInterceptor)
export class RegisterController {
@Post()
register(@Body() register: RegisterDto) {
console.log(`Register parameters: ${JSON.stringify(register)}`);
return 'This method adds a new user';
}
}

When we inject our interceptor by using @UseInterceptors() decorator, our audit log class will be ready for each routes without adding anything to the controller class. That’s the nice part of AOP/NestJS Interceptor.

Let’s run the application. Head back to terminal, run the code with $ npm run start and the output on the console will be as follows, when call the register endpoint with HTTP Post method:

[Nest] 27710   - 10/22/2020, 15:43:17   [RouterExplorer] Mapped {/notification, GET} route +1ms
[Nest] 27710 - 10/22/2020, 15:43:17 [NestApplication] Nest application successfully started +6ms
Before..
Register parameters: {}
After... 3ms

As you see above, to use an interceptor with @UseInterceptors() decorator in the controller is quite simple, so like that in general we have interceptors that can be

  • Controller-scoped
  • Method-scoped
  • Global-scoped

Controller-scoped interceptors can be as follows and interceptors will be triggered for each routes:

@UseInterceptors(AuditLogInterceptor)
export class RegisterController {}

But if you want to trigger an interceptor for only one route, we simply apply @UseInterceptors() decorator to a method level as shown below:

register.controller.ts

@Controller('register')
export class RegisterController {
@Post()
register(@Body() register: RegisterDto) {
console.log(`Register parameters: ${JSON.stringify(register)}`);
return 'This method adds a new user';
}
@Get()
@UseInterceptors(AuditLogInterceptor)
getRegistirationStatus() {
console.log(`get method is executed.`);
return 'get method is executed.';
}
}

Global-scoped interceptor are used across the whole application(e.g. for every controller, for every route handler) and can be set up by using NestJS application’s useGlobalIntercetors() method as follows:

const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new AuditLogInterceptor ());

Response mapping

Interceptors are very useful to manage all of our responses from one place, too. For instance, we can easily change all of our null and undefined responses with empty(‘’) string as follows:

null-and-undefined.interceptor.ts

import {
Injectable, NestInterceptor, ExecutionContext, CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ExcludeNullAndUndefinedInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
map(value => (value === null || value === 'undefined' ? '' : value)),
);
}
}

Exception mapping

We can handle errors by using interceptors as shown below. That kind of exception handling can be useful for mapping of client’s errors.

.pipe(catchError(err => throwError(new BadGatewayException())));

Stream overriding

We can easily build a cache mechanism for our application by using interceptors. Sample example as shown below, but of course for a real application, we may implement more realistic caching interceptor :

import { Injectable, NestInterceptor, ExecutionContext,  CallHandler, } from '@nestjs/common';
import { Observable, of } from 'rxjs';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of(['Cache data']);
}
return next.handle();
}
}

Resources:

[1] Aspect Oriented Programming, Wikipedia, 2020, available at https://en.wikipedia.org/wiki/Aspect-oriented_programming

--

--

Rasim Sen/Blockchain Architect-zero2hero founder
Rasim Sen/Blockchain Architect-zero2hero founder

Written by Rasim Sen/Blockchain Architect-zero2hero founder

I am a blockchain architect, close on 2 decades of experience as a developer, I’ve got 5 products in blockchain, and I like to talk about tech,sci-fi, futurism!