nestjsclass-validatorcustom-errors

NestJS ValidationPipe with stopAtFirstError: true doesn't respect the order of decorators in class-validator


I'm using NestJS with class-validator to validate incoming requests. In my SignUpDto class, I have applied validation decorators like IsNotEmpty, IsString, and IsEmail to validate the email and password fields. I want the validation error to be thrown in the same order as the decorators are applied but throw only one the erorrs (if ).

To achieve this, i am using exceptionFactory to throw the only one error in the app.useGlobalPipes configuration. However, the errors are not thrown in the expected order(bacause i am returning only the error at index 0)

i there any way solution so i can thow the error in the samee order but one error at a time?

// SignUpDto.ts
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';

export class SignUpDto {
  @IsNotEmpty({ message: 'Email should not be empty' })
  @IsString({ message: 'Invalid email format' })
  @IsEmail({}, { message: 'Invalid email format' })
  email: string;

  @IsNotEmpty({ message: 'Password should not be empty' })
  @IsString({ message: 'Invalid password format' })
  @MinLength(8, { message: 'Password must be at least 8 characters long' })
  password: string;
}

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {
  BadRequestException,
  HttpException,
  HttpStatus,
  ValidationPipe,
} from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors({
    origin: true,
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
    credentials: true,
  });
  app.useGlobalPipes(
    new ValidationPipe({
      stopAtFirstError: true,
      whitelist: true,
      exceptionFactory: (errors) => {
        const messages = errors[0].constraints; 
        const message = Object.values(messages)[0];
        const response = { message, statusCode: HttpStatus.BAD_REQUEST };
        throw new HttpException(response, HttpStatus.BAD_REQUEST);
      },
    }),
  );
  await app.listen(5001);
}
bootstrap();


Solution

  • You are using TypeScript decorators (the ones you import from class-validator) to add the validation for your DTOs.

    TypeScript decorators are executed bottom-to-top.

    When multiple decorators apply to a single declaration, their evaluation is similar to function composition in mathematics. In this model, when composing functions f and g, the resulting composite (f ∘ g)(x) is equivalent to f(g(x)).

    As such, the following steps are performed when evaluating multiple decorators on a single declaration in TypeScript:

    1. The expressions for each decorator are evaluated top-to-bottom.
    2. The results are then called as functions from bottom-to-top.

    https://www.typescriptlang.org/docs/handbook/decorators.html#decorator-composition

    That means in your given DTO:

    export class SignUpDto {
      @IsNotEmpty({ message: 'Email should not be empty' })
      @IsString({ message: 'Invalid email format' })
      @IsEmail({}, { message: 'Invalid email format' })
      email: string;
    }
    

    The execution order of your decorators will be:

    1. IsEmail
    2. IsString
    3. IsNotEmpty

    I'm assuming you want it the other way. That means, you need to adjust the decorator order to:

    export class SignUpDto {
      @IsEmail({}, { message: 'Invalid email format' })
      @IsString({ message: 'Invalid email format' })
      @IsNotEmpty({ message: 'Email should not be empty' })
      email: string;
    }
    

    This should fix the order for you.


    For your second query of returning only 1 error, your approach looks good. Using both stopAtFirstError and executionFactory will be the easiest way.