NestJS what is the best practice to initialise and pass request context

Viewed 11374

I have a global interceptor that needs to initialize my own request context DTO and I want this DTO to be accessible in Controller which handles the current request.

The solution i found so far is to create Request scoped injectable RequestContext class:

import {
    Injectable,
    Scope
} from '@nestjs/common';
import { Request } from 'express';
import { IncomingHttpHeaders } from 'http';

@Injectable({ scope: Scope.REQUEST })
export class RequestContext {
    public headers: IncomingHttpHeaders;
    ....

    initialize(request: Request) {
        this.headers = request.headers;
        .....
    }
}

And inject this class into the interceptor:

import {
    NestInterceptor,
    ExecutionContext,
    CallHandler,
    Injectable,
    Inject
} from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { RequestContext } from '../dto';

@Injectable()
export class RequestContextInterceptor implements NestInterceptor {
    constructor(
        @Inject(RequestContext)
        protected requestContext: RequestContext
    ) { }

    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        const request = context.switchToHttp().getRequest<Request>();
        this.requestContext.initialize(request);

        return next.handle()
            .pipe(
                tap(() => {
                    // decorate response
                }));
    }
}

And then inject this RequestContext in each controller...

import {
    Controller,
    UseInterceptors,
    Inject,
    Get
} from '@nestjs/common';
import { BaseMicroserviceController } from '../core/base/base-microservice.controller';
import { RequestContext } from '../dto';
import { DispatchService } from '../services';

@Controller('api/v1/example')
export class ExampleController extends BaseMicroserviceController {

    constructor (
        @Inject(RequestContext)
        protected requestContext: RequestContext,
        protected dispatcheService: DispatchService
    ) {
        super(dispatcheService);
    }

    @Get()
    test() {
        return 'test';
    }
}

There is huge workaround to achieve this simple functionality IMHO In addition, I have this article which describes why to use Scope based injection is not good: https://guxi.me/posts/why-you-should-avoid-using-request-scope-injection-in-nest-js/

My service will be huge, with a huge amount of controllers and a huge amount of injectable services. According to this article - my service will be not scalable in terms of performance and memory usage.

My question is how to achieve the functionality I need in NestJS and what is the best practice? Another "bonus question" - the RequestContext class has initialize method which receives express Request and parses it. I don't like it, I want each property of this class to be read-only and initialize this class in a traditional way by calling the constructor with request object... How I can achieve it with @Inject strategy?

4 Answers

If you want to do this without using Request Scoped providers you can simplify a lot of it by just enriching the request object with additional data. The request object is technically always available for an inbound HTTP interaction regardless of what Injection Scope you're using. You can ditch the RequestContext entirely and just add whatever additional data you want to the request object inside your Interceptor.

 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest<Request>();
    const customRequestContext = initialize(request); // whatever you need to do to build this

    request.customRequestContext = customRequestContext;

    return next.handle();
}

It is easy to get access to this value in any of your controllers using a custom decorator:

export const RequestContext = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.customRequestContext;
  },
);

And then in any of your controllers you can use this to get access to the value:

@Get()
async findOne(@RequestContext() requestContext: RequestContextInterface) {
  // do whatever you need to do with it in your controllers
}

An approach for sharing a request context is to use a Continuation-local storage (CLS) package like express-http-context. You can then

  • use a middleware to set the relevant context data
  • define a provider that wraps the CLS logic
  • inject this provider in you controllers
// request-context.ts
import { createParamDecorator, HttpException, HttpStatus, Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import httpContext from 'express-http-context';

@Injectable()
export class RequestContextProvider {
    get(key) {
        return httpContext.get(key)
    }

    set(key, value) {
        return httpContext.set(key, value)
    }
}


@Injectable()
export class RequestContextMiddleware implements NestMiddleware {
    constructor(private requestContextProvider: RequestContextProvider) { }

    use(req: Request, res: Response, next: NextFunction) {

        // first run express-http-context middleware
        httpContext.middleware(req, res, () => {
            // set context data
            // for example extract user data from JWT
            const [, token] = req.headers.authorization.split(' ')
            const decoded: Record<string, unknown> = jwt_decode(token)
            this.requestContextProvider.set('userId', decoded.userId)
            next();
        })
    }
}

Appy middleware in the app

//app.module.ts

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { RequestContextMiddleware, RequestContextProvider } from './common/request-context';
import { MyModule } from './my/my.module';

@Module({
    imports: [MyModule],
    providers: [RequestContextProvider]
})
export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer
            .apply(RequestContextMiddleware)
            .forRoutes('*');
    }
}

Inject in controllers like this

//my.controller.ts

import { Controller, Post } from '@nestjs/common';

@Controller('my')
export class MyController {
    constructor(private requestContextProvider: RequestContextProvider) { }
    @Post()
    doSomething(): string {
        const userId = this.requestContextProvider.get('userId')
        //  do something with user ID
        //...
    }
}

You can also use decorators


// request-context.ts

export const UserId = createParamDecorator(() => {
    const userId = httpContext.get('userId')
    if (!userId) {
        throw new HttpException(
            'Missing authorisation credentials',
            HttpStatus.FORBIDDEN
        )
    }
    return userId;
})

The full NestJS shared request context example

As far as I understood you want a middleware that manipulates request. For this u can create a custom Interceptor.

import {
  UseInterceptors,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';

//  nest will take dto and turn it to the json and send it back as response

// with implement, this class will satisfy either an abstract class or an interface
export class MyCustomInterceptor implements NestInterceptor {
    // passing dto will make this class reusable
    constructor(private dto:any){

    }
  intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {

    // this is where  you run code before request is handled
    return handler.handle().pipe(
      
      map((data: any) => {
         // data is the response
         // here you can manipulate the response
        });
      }),
    );
  }
}

you use this in controller like this:

 import {UseInterceptors} from '@nestjs/common';


// U can manipulate the request for this route
@UseInterceptors(new MyCustomInterceptor(YourDto))
  // each part of the request is string. thats why param is string
  @Get('/:id')
  async findUser(@Param('id') id: string) {
   // logic...
  }

I think you can use nj-request-scope package I wrote. It's free from bubbles up injection chain and performance issues:

https://github.com/kugacz/nj-request-scope

First, you have to add import of RequestScopeModule in the module class decorator:

import { RequestScopeModule } from 'nj-request-scope';

@Module({
    imports: [RequestScopeModule],
})

and then your RequestContext class can be written like this:

import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { IncomingHttpHeaders } from 'http';
import { RequestScope } from 'nj-request-scope';
import { NJRS_REQUEST } from 'nj-request-scope';

@Injectable()
@RequestScope()
export class RequestContext {
    public headers: IncomingHttpHeaders;
    ....

    constructor(@Inject(NJRS_REQUEST) private readonly request: Request) {
        this.headers = request.headers;
    }
}

In that case, a new instance of RequestContext class will be created for every request. So, you can store any request-scope information inside this class. Injecting this class into any part of your application won't cause a bubble up injection chain.

Another approach is like this:

import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { IncomingHttpHeaders } from 'http';
import { NJRS_REQUEST } from 'nj-request-scope';

@Injectable()
export class RequestContext {

    constructor(@Inject(NJRS_REQUEST) private readonly request: Request) {
    }

    public get headers(): IncomingHttpHeaders {
        return this.request.headers;
    }
}

In that case RequestContext class is singleton-scope and only request field is request-scope. So, you can't store any request-scoped information directly in RequestContext class rather you have to read request-scoped data directly from the request object via getters. Also, injecting this class into any part of your application won't cause a bubble up injection chain.

Simple nj-request-scope package usage examples you can find here: https://github.com/kugacz/nj-request-scope-example

Related