Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Throw same error format as `class-validator` in NestJS

Let's have this controller in NestJS project:

  @Post('resetpassword')
  @HttpCode(200)
  async requestPasswordReset(
    @Body() body: RequestPasswordResetDTO,
  ): Promise<boolean> {
    try {
      return await this.authService.requestPasswordReset(body);
    } catch (e) {
      if (e instanceof EntityNotFoundError) {
        // Throw same exception format as class-validator throwing (ValidationError)
      } else throw e;
    }
  }

Dto definition:

export class RequestPasswordResetDTO {
  @IsNotEmpty()
  @IsEmail()
  public email!: string;
}

I want to throw error in ValidationError format (property, value, constraints, etc) when this.authService.requestPasswordReset(body); throws an EntityNotFoundError exception.

How I can create this error manually? Those errors are just thrown when DTO validation by class-validator fails. And those can be just static validations, not async database validations.

So the final API response format should be for example:

{
    "statusCode": 400,
    "error": "Bad Request",
    "message": [
        {
            "target": {
                "email": "[email protected]"
            },
            "value": "[email protected]",
            "property": "email",
            "children": [],
            "constraints": {
                "exists": "email address does not exists"
            }
        }
    ]
}

I need it to have consistent error handling :)

like image 796
Baterka Avatar asked Feb 17 '20 21:02

Baterka


3 Answers

When adding the ValidationPipe to your app, provide a custom exceptionFactory:

  app.useGlobalPipes(
    new ValidationPipe({
      exceptionFactory: (validationErrors: ValidationError[] = []) => {
        return new BadRequestException(validationErrors);
      },
    })
  );

This should be all you need to get the intended result.

For comparison, you can check out the original NestJS version here.

like image 180
Joshua Avatar answered Nov 11 '22 05:11

Joshua


You could use an Exception Filter to create your customized response to that exception First we define the Exception Filter:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
// import { EntityNotFoundError } from 'wherever';

@Catch(EntityNotFoundError)
export class EntityNotFoundExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        "statusCode": 400,
        "error": "Bad Request",
        "message": [
          {
            "target": {},
            "property": "email",
            "children": [],
            "constraints": {
              "isEmail": "email must be an email"
            }
          },
          // other field exceptions
        ]
      });
  }
}

Then back in your controller, you use the filter:

  // ...
  import { EntityNotFoundExceptionFilter } from 'its module';
  // ...
  @Post('resetpassword')
  @HttpCode(200)
  @UseFilters(EntityNotFoundExceptionFilter)
  async requestPasswordReset(
    @Body() body: RequestPasswordResetDTO
  ): Promise<boolean> {
      return await this.authService.requestPasswordReset(body);
  }

This should work just fine.

like image 24
toondaey Avatar answered Nov 11 '22 04:11

toondaey


We can get back the exception response thrown by class-validator and set to response,

import {
  ArgumentsHost,
  BadRequestException,
  Catch,
  ExceptionFilter
} from '@nestjs/common';

@Catch()
export class ValidationFilter < T > implements ExceptionFilter {
  catch (exception: T, host: ArgumentsHost) {
    if (exception instanceof BadRequestException) {
      const response = host.switchToHttp().getResponse();
      response.status(exception.getStatus())
        .json(exception.getResponse());
    }
  }
}

Controller should look,

@Post('create')
@UsePipes(ValidationPipe)
@UseFilters(ValidationFilter)
async create(@Body() body: CreateDto) {

}
like image 23
HenonoaH Avatar answered Nov 11 '22 05:11

HenonoaH