Throw same error format as `class-validator` in NestJS
Asked Answered
R

5

17

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 :)

Roan answered 17/2, 2020 at 21:13 Comment(1)
You can create an async validator. See example here: github.com/typestack/…Hyozo
R
26

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.

Rafaello answered 30/4, 2020 at 14:16 Comment(4)
I think at one point NestJs use to always return ValidationErrors, but in the latest version they defaulted to their own exception filter. I could be wrong, but I did have an array of ValidationErrors coming across at one point. That or I changed the config somewhere.Bought
And how do you access the values?Unlearned
Not entirely sure what you're asking, @John. Inside the exception factory arrow function, the raw validation errors from class-validator can be accessed in the validationErrors variable.Rafaello
You may need to concatenate the constraints to pass into the Exception constructor to see the validation descriptions.Blessing
S
2

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.

Strained answered 18/2, 2020 at 3:46 Comment(2)
EntityNotFoundError is not same as HttpException, your example will fail on exception.getStatus()Roan
This is pretty bad. You would need to create a custom exception filter for every single error message in class-validator. That would be like 30+ exception filters. No thanks!Unlearned
B
2

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) {

}
Briones answered 25/2, 2022 at 13:17 Comment(0)
N
1

Extend the default validation pipe and overwrite the createExceptionFactory function:

import {
  ValidationError,
  ValidationPipe as NestValidationPipe,
} from '@nestjs/common';
import { HttpErrorByCode } from '@nestjs/common/utils/http-error-by-code.util';

export class ValidationPipe extends NestValidationPipe {
  public createExceptionFactory() {
    return (validationErrors: ValidationError[] = []) => {
      if (this.isDetailedOutputDisabled) {
        return new HttpErrorByCode[this.errorHttpStatusCode]();
      }
      return new HttpErrorByCode[this.errorHttpStatusCode](validationErrors);
    };
  }
}

What this does is just to remove a single line of code from the default ValidationPipe that converts the errors: https://github.com/nestjs/nest/blob/4ebe4504b938faff615a9c95abc4a185419e304c/packages/common/pipes/validation.pipe.ts#L123

Ningsia answered 7/11, 2022 at 9:26 Comment(0)
W
0

I know this question is old and the OP has probably moved on, but I keep hitting it as the top search result, and from my understanding, no-one has addressed the OP's question. I have the same requirement. I need to throw an Exception in the same format as class validators validation exceptions. For that purpose, I created a custom exception class:

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

export class ValidationException extends BadRequestException {
  constructor(
    propertyName: string,
    message: string,
    constraintName = 'custom',
    originalData = {},
  ) {
    // wrap arguments in the correct error format
    super([
      {
        target: originalData,
        property: propertyName,
        value: originalData[propertyName] || null,
        children: [],
        constraints: {
          [constraintName]: message,
        },
      },
    ]);
  }
}

Then whenever I need to throw an exception from my controller, I can do:

@NoAuth()
  @Post('login')
  async signIn(@Body() signInDto: LoginUserDto): Promise<ApiResponse> {

    if (isNotFoo(signInDto.email)){
        throw new ValidationException(
          'email',
          'Email is not FOO!',
          'mustBeFoo', // optional, will default to custom
          signInDto, // optional, include it if your front-end relies on the 'target' field, or the 'value' field of the response
        );
    }
    
  }
Washroom answered 12/9, 2023 at 3:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.