typescript - NestJS vad är bästa praxis för att initialisera och skicka begärandekontext


Jag har en global interceptor som behöver initiera min egen request context DTO och jag vill att denna DTO ska vara tillgänglig i Controller som hanterar den aktuella förfrågan.

Lösningen jag har hittat hittills är att skapa en Request-omfattad injicerbar RequestContext-klass:

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;
        .....
    }
}

Och injicera denna klass i interceptorn:

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
                }));
    }
}

Och sedan injicera detta RequestContext i varje 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';
    }
}

Det finns en stor omväg för att uppnå denna enkla funktionalitet enligt min åsikt. Dessutom har jag denna artikel som beskriver varför det inte är bra att använda Scope-baserad injektion: https://guxi.me/posts/why-you-should-avoid-using-request-scope-injection-in-nest-js/

Min tjänst kommer att vara enorm, med ett enormt antal kontroller och ett enormt antal injicerbara tjänster. Enligt denna artikel kommer min tjänst inte att vara skalbar när det gäller prestanda och minnesanvändning.

Min fråga är hur man uppnår den funktionalitet jag behöver i NestJS och vad som är bästa praxis? En annan "bonusfråga" - klassen RequestContext har metoden initialize som tar emot en express Request och parser den. Jag gillar inte det, jag vill att varje egenskap i denna klass ska vara skrivskyddad och initiera denna klass på ett traditionellt sätt genom att anropa konstruktorn med request-objektet... Hur kan jag uppnå detta med @Inject-strategin?

Svar #1

Om du vill göra detta utan att använda Request Scoped-leverantörer kan du förenkla mycket av det genom att bara berika request-objektet med ytterligare data. Request-objektet är tekniskt sett alltid tillgängligt för en inkommande HTTP-interaktion oavsett vilken Injection Scope du använder. Du kan helt enkelt slopa RequestContext och bara lägga till vilken ytterligare data du vill till request-objektet inne i din 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();
}

Det är enkelt att få tillgång till detta värde i någon av dina controllers genom att använda en anpassad dekoratör:

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

Och sedan kan du i någon av dina kontroller använda detta för att få tillgång till värdet:

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

Svar #2

Ett tillvägagångssätt för att dela en begäranskontext är att använda ett Continuation-local storage (CLS)-paket som express-http-context. Du kan sedan

  • använd en middleware för att sätta relevant kontextdata
  • definiera en leverantör som omsluter CLS-logiken
  • injicera den här leverantören i dina kontroller
// 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();
        })
    }
}

Applicera middleware i appen

//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('*');
    }
}

Injicera i kontroller på detta sätt

//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
        //...
    }
}

Du kan också använda dekoratörer

// request-context.ts

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

Det fullständiga NestJS-exemplet med delad begärandekontext

Relaterade artiklar