Search code examples
node.jsnestjsnestjs-fastify

NestJS: How can I access controller function response using a custom decorator?


This is my decorator

import { createParamDecorator, ExecutionContext } from "@nestjs/common";

export const CacheData = createParamDecorator(
    (data: any, ctx: ExecutionContext) => {
        const request = ctx.switchToHttp().getRequest();
        console.log(request.url, request.method, 'request');
        const response = ctx.switchToHttp().getResponse();
        const reqBody = request.body;
        console.log(reqBody, 'reqBody');
        console.log(response.raw.req.data, 'response');
        console.log(response.raw.req.body, 'response');
        console.log(response.raw.data, 'response');
        console.log(response.cacheData, 'response');
    }
);

in my controller function I'm using the decorator as follows:

getStringsArr(
    @Headers('Authorization') auth: string,
    @Headers('Country') country = 'DK',
    @CacheData() cacheData,
  ): Array<string> {
return ['Hello', 'World', '!'];
}

so how can I access response data in my CacheData decorator?


Solution

  • Your CacheData decorator is a param decorator, which means, as far as I know, that it will be executed only when the method handler is called. You have a few options. Here are two.

    Option A - Method decorator

    A method decorator would give you access to the returned data from your function but comes with drawbacks: you don't have access to the execution context, unlike the param decorator and injecting customer services is less elegant. I like this option because it is easy to supply parameters to the decorator.

    Since your example is around caching, I suspect you'll want to inject your service there, so option B is probably more fitted to your requirements, but here's an implementation with a method decorator :

    const MyMethodDecorator = (params) => {
      return (
        target: Record<string, unknown>,
        _propertyKey: string,
        descriptor: PropertyDescriptor,
      ) => {
        const originalMethod = descriptor.value;
        descriptor.value = async function (...args) {
          const data = await originalMethod.apply(this, args);
          // data = ['Hello', 'World', '!'] 
        };
    
        return descriptor;
      };
    };
    
    @MyMethodDecorator({ ttl: 120, cacheKey: 'stringsArr' })
    getStringsArr(
      @Headers('Authorization') auth: string,
      @Headers('Country') country = 'DK'
    ): Array<string> {
      return ['Hello', 'World', '!'];
    }
    

    Option B - Route Interceptor

    Interceptors make dependency injection easy since it's like any other NestJS service. I recommend reading and understanding the request lifecycle is you choose this option.

    One drawback vs a decorator is that supplying parameters is less elegant but is doable using reflection and a method decorator:

    import { applyDecorators, SetMetadata, UseInterceptors } from '@nestjs/common';
    
    @Injectable()
    export class MyInterceptor implements NestInterceptor {
    
      constructor(private readonly reflector: Reflector) {}
    
      intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        const params = this.reflector.get('MyInterceptorMetadataKey', context.getHandler());
        // params = { ttl: 120, cacheKey: 'stringsArr' }
    
        return next.handle().pipe(
          tap((response) => {
            // data = ['Hello', 'World', '!'] 
          }),
        );
      }
    }
    
    const MyMethodDecorator = (params) => 
        applyDecorators(SetMetadata('MyInterceptorMetadataKey', params));
    
    @UseInterceptors(MyInterceptor)
    @MyMethodDecorator({ ttl: 120, cacheKey: 'stringsArr' })
    getStringsArr(
      @Headers('Authorization') auth: string,
      @Headers('Country') country = 'DK'
    ): Array<string> {
      return ['Hello', 'World', '!'];
    }