Search code examples
nestinterceptor

Use custom interceptors for the response


I'm using a global interceptor to get response like:

{
  "data": "",
  "statusCode": int
  "message": "string"
}

so I created the interceptor file

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { map, Observable } from "rxjs";

export interface Response<T> {
    data: T;
}

@Injectable()
export class TransformationInterceptor<T> implements NestInterceptor<T, Response<T>> {
    intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
        return next.handle().pipe(map(data => ({ 
            data: data,
            statusCode: context.switchToHttp().getResponse().statusCode,
            message: data.message
        })));
    }
}

and put it into my main.ts

In my controller I have:

  @Patch('/:userId')
  @HttpCode(201)
  public async updateUser(    
    @Param('userId') userId: string,
    @Body() userUpdate: UpdateUserDto): Promise<any> {      
    return await this.usersService.update(userId, userUpdate);    
  }

and the result is:

{
  "data": {
    "_id": "621d07d9ea0cdc600fae0f02",    
    "username": "foo",
    "name": "stringwwww",
    "__v": 0
  },
  "statusCode": 201
}

If I want to add my custom message, I need to return an object like:

@Patch('/:userId')
  @HttpCode(201)
  public async updateUser(    
    @Param('userId') userId: string,
    @Body() userUpdate: UpdateUserDto): Promise<any> {      
    const result = await this.usersService.update(userId, userUpdate);    
    return { message: 'User updated', result };    
  }

but in that case I have twice message and the structure is not correct:

{
  "data": {
    "message": "User updated",
    "result": {
      "_id": "621d07d9ea0cdc600fae0f02",
      "username": "foo",
      "name": "stringwwww",
      "__v": 0
    }
  },
  "statusCode": 201,
  "message": "User updated"
}

How can I set a custom (optional) message?

I can modify my interceptors like:
@Injectable()
export class TransformationInterceptor<T> implements NestInterceptor<T, Response<T>> {
    intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
        return next.handle().pipe(map(data => ({ 
            data: data.res,
            statusCode: context.switchToHttp().getResponse().statusCode,
            message: data.message
        })));
    }
}

and my controller like:

@Patch('/:userId')
  @HttpCode(201)
  public async updateUser(    
    @Param('userId') userId: string,
    @Body() userUpdate: UpdateUserDto): Promise<any> {      
    const result = await this.usersService.update(userId, userUpdate);    
    return { message: 'User updated', res: result };    
  }

and I will get the correct form, but I don't want to add

return { message: 'User updated', res: result };    

for each controller


Solution

  • One way to achieve this is as below, but you will be bound to a fixed message per controller

    Create a response decorator (response.decorator.ts)

        import { SetMetadata } from '@nestjs/common'
        
        export const ResponseMessageKey = 'ResponseMessageKey'
        export const ResponseMessage = (message: string) => SetMetadata(ResponseMessageKey, message)
    

    Create a constants file for your responses (response.constants.ts)

        export const USER_INSERTED = 'User Inserted'
        export const USER_UPDATED = 'User Updated'
        export const USER_DELETED = 'User Deleted'
    

    Add the decorator to your controller to set response message metadata

        @Patch('/:userId')
        @HttpCode(201)
        @ResponseMessage(USER_UPDATED)
        public async updateUser(    
          @Param('userId') userId: string,
          @Body() userUpdate: UpdateUserDto
        ): Promise<any> {      
          const result = await this.usersService.update(userId, userUpdate);    
          return result;    
        }
    

    Update your interceptor to read the response message from the metadata set on the controller and add it in the response

        import { Reflector } from '@nestjs/core'
    
        @Injectable()
        export class TransformationInterceptor<T>
          implements NestInterceptor<T, Response<T>>
        {
          constructor(private reflector: Reflector) {}
    
          intercept(
            context: ExecutionContext,
            next: CallHandler
          ): Observable<Response<T>> {
            const responseMessage = this.reflector.get<string>(
              ResponseMessageKey,
              context.getHandler()
            ) ?? ''
    
            return next.handle().pipe(
              map((data) => ({
                data,
                statusCode: context.switchToHttp().getResponse().statusCode,
                message: responseMessage
              }))
            )
          }
        }
    

    You could extend this approach to set a list of strings/objects as possible responses (metadata), and based on response code in interceptor, send a particular message as response.message