How can I use validation groups for DTOs in a NestJS controller?

Viewed 1276

I want to use validation groups in my DTO and use them in my controller. Does anyone know how you can use/pass validation groups to the controller or have the request.user automatically pass it to the controller?

create-user.dto.ts

export class CreateUserDto {
  @IsNotEmpty({
    groups: ["admin"],
  })
  role: string;

  @IsNotEmpty()
  @IsEmail()
  email: string;

  @IsNotEmpty()
  password: string;
}

users.controller.ts

@Controller('users')
@UseGuards(JwtAuthGuard)
@UseInterceptors(ClassSerializerInterceptor)
export class UsersController {
  constructor(
    private readonly usersService: UsersService,
  ) {}

  @Post()
  async create(@Body() createUserDto: CreateUserDto): Promise<User> {
    const result = await this.usersService.create(createUserDto);
    return result;
  }
}
2 Answers

@IsNotEmpty and @IsEmail decorators are coming from the class-validator library, so you should use ValidationPipe

Nest JS docs about validation: https://docs.nestjs.com/techniques/validation

Updated users.controller.ts:

@Controller('users')
@UseGuards(JwtAuthGuard)
export class UsersController {
  constructor(
    private readonly usersService: UsersService,
  ) {}

  @Post()
  @UsePipes(new ValidationPipe({ groups: ["admin"] }))
  async create(@Body() createUserDto: CreateUserDto): Promise<User> {
    const result = await this.usersService.create(createUserDto);
    return result;
  }
}

Sharing my draft for this one. As there are not much information around how to dynamicly set validation group.

First of all, create method generateGroups, inside the DTO. Put corresponding logic to fill validation groups. In my example I am using field name as group.

enum APP_SETTING {
    MAINTENANCE_MODE = "maintenance-mode",
    RECONNECTION_DELAY = "reconnection-delay"
}

export class EditSettingDto {
    protected readonly generateGroups? = () => {
        const groups = [];
        if (Object.values(APP_SETTING).includes(this.name)) {
            groups.push(this.name);
        }
        return groups;
    };

    @IsNotEmpty()
    @IsEnum(APP_SETTING, { always: true })
    name: APP_SETTING;

    @IsNotEmpty()
    @IsBoolean({ groups: [APP_SETTING.MAINTENANCE_MODE] })
    @IsInt({ groups: [APP_SETTING.RECONNECTION_DELAY] })
    value: any;
}

Afterwards you may have custom validation pipe which should have execution priority over global pipe. In my case I compose ValidationPipe with custom transform logic.

Check for existance of generateGroups method in the DTO and collect all groups by calling it and passing groups to the ValidationPipe instance. Run validationPipe.transform with input arguments. That's it!

export class CustomValidationPipe implements PipeTransform {
    constructor(private readonly options?: ValidationPipeOptions) {}

    transform(value: any, metadata: ArgumentMetadata): Promise<any> {
        const { metatype } = metadata;

        const instance = plainToInstance(metatype, value);

        let groups: string[] = [];
        if (typeof instance.generateGroups === "function") {
            groups = instance.generateGroups();
        }

        const validationPipe = new ValidationPipe({
            ...this.options,
            groups
        });

        return validationPipe.transform(value, metadata);
    }
}

Main.ts

  app.useGlobalPipes(
        new CustomValidationPipe({
            whitelist: true
        })
    );
Related