Skip to Content

Validation

NestJS CRUD Automator provides comprehensive validation capabilities through class-validator integration and custom validators.

Automatic Validation

Validation rules are automatically applied based on entity property configuration:

user.entity.ts
@Entity("users") export class UserEntity { @Column() @ApiPropertyDescribe({ type: EApiPropertyDescribeType.STRING, description: "Username", format: EApiPropertyStringType.STRING, minLength: 3, // Adds @MinLength(3) maxLength: 20, // Adds @MaxLength(20) pattern: "/^[a-zA-Z0-9_-]+$/", // Adds @Matches(/^[a-zA-Z0-9_-]+$/) isRequired: true, // Adds @IsNotEmpty() }) username: string; @Column() @ApiPropertyDescribe({ type: EApiPropertyDescribeType.STRING, description: "Email", format: EApiPropertyStringType.EMAIL, // Adds @IsEmail() isRequired: true, }) email: string; @Column() @ApiPropertyDescribe({ type: EApiPropertyDescribeType.NUMBER, description: "Age", format: EApiPropertyNumberType.INTEGER, // Adds @IsInt() minimum: 18, // Adds @Min(18) maximum: 120, // Adds @Max(120) }) age: number; }

Built-in Validators

The library provides several built-in cross-field validators:

AllOrNoneOfListedPropertiesValidator

Ensures either all or none of the specified properties are provided:

user.controller.ts
import { AllOrNoneOfListedPropertiesValidator } from "@elsikora/nestjs-crud-automator"; @ApiController<UserEntity>({ entity: UserEntity, routes: { [EApiRouteType.CREATE]: { autoDto: { [EApiDtoType.BODY]: { validators: [ { constraintClass: AllOrNoneOfListedPropertiesValidator, options: ["firstName", "lastName"], }, ], }, }, }, }, })

OnlyOneOfListedPropertiesValidator

Ensures exactly one of the specified properties is provided:

import { OnlyOneOfListedPropertiesValidator } from "@elsikora/nestjs-crud-automator"; validators: [ { constraintClass: OnlyOneOfListedPropertiesValidator, options: ["email", "phone"], }, ]

HasAtLeastOneOfListedPropertiesValidator

Requires at least one of the specified properties:

import { HasAtLeastOneOfListedPropertiesValidator } from "@elsikora/nestjs-crud-automator"; validators: [ { constraintClass: HasAtLeastOneOfListedPropertiesValidator, options: ["email", "phone", "username"], }, ]

HasAtLeastOneAndOnlyOneOfListedPropertiesValidator

Requires exactly one of the specified properties:

import { HasAtLeastOneAndOnlyOneOfListedPropertiesValidator } from "@elsikora/nestjs-crud-automator"; validators: [ { constraintClass: HasAtLeastOneAndOnlyOneOfListedPropertiesValidator, options: ["cardNumber", "paypalEmail", "bankAccount"], }, ]

MustMatchOneOfSchemasValidator

Validates against one of multiple possible schemas:

import { MustMatchOneOfSchemasValidator } from "@elsikora/nestjs-crud-automator"; validators: [ { constraintClass: MustMatchOneOfSchemasValidator, options: [CardPaymentSchema, PayPalPaymentSchema], }, ]

HasPairedCustomSuffixesFieldsValidator

Validates paired fields with custom suffixes:

import { HasPairedCustomSuffixesFieldsValidator } from "@elsikora/nestjs-crud-automator"; validators: [ { constraintClass: HasPairedCustomSuffixesFieldsValidator, options: { baseName: "address", suffixes: ["Street", "City", "Country"], }, }, ]

Custom Validators

Create custom validators for specific requirements:

validators/is-strong-password.validator.ts
import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from "class-validator"; @ValidatorConstraint({ async: false }) export class IsStrongPasswordConstraint implements ValidatorConstraintInterface { validate(password: string, args: ValidationArguments) { if (!password) return false; const hasUpperCase = /[A-Z]/.test(password); const hasLowerCase = /[a-z]/.test(password); const hasNumber = /[0-9]/.test(password); const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); const isLongEnough = password.length >= 8; return hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar && isLongEnough; } defaultMessage(args: ValidationArguments) { return "Password must contain at least 8 characters, including uppercase, lowercase, number, and special character"; } } export function IsStrongPassword(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [], validator: IsStrongPasswordConstraint, }); }; }

Use custom validator:

import { IsStrongPassword } from "./validators/is-strong-password.validator"; @Column() @ApiPropertyDescribe({ type: EApiPropertyDescribeType.STRING, description: "Password", format: EApiPropertyStringType.PASSWORD, }) @IsStrongPassword() password: string;

Async Validators

Create async validators for database checks:

validators/is-username-unique.validator.ts
import { Injectable } from "@nestjs/common"; import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from "class-validator"; import { UserService } from "../user/user.service"; @ValidatorConstraint({ async: true }) @Injectable() export class IsUsernameUniqueConstraint implements ValidatorConstraintInterface { constructor(private readonly userService: UserService) {} async validate(username: string, args: ValidationArguments): Promise<boolean> { const user = await this.userService.repository.findOne({ where: { username } }); return !user; } defaultMessage(args: ValidationArguments) { return "Username already exists"; } }

Conditional Validation

Validate fields conditionally:

dto/create-payment.dto.ts
import { ValidateIf, IsNotEmpty } from "class-validator"; export class CreatePaymentDto { @IsNotEmpty() type: "card" | "paypal"; @ValidateIf(o => o.type === "card") @IsNotEmpty() cardNumber?: string; @ValidateIf(o => o.type === "card") @IsNotEmpty() cvv?: string; @ValidateIf(o => o.type === "paypal") @IsNotEmpty() paypalEmail?: string; }

Nested Object Validation

Validate nested objects:

import { ValidateNested, Type } from "class-transformer"; class AddressDto { @IsNotEmpty() street: string; @IsNotEmpty() city: string; @IsNotEmpty() country: string; } @Column({ type: "jsonb" }) @ApiPropertyDescribe({ type: EApiPropertyDescribeType.OBJECT, description: "User address", }) @ValidateNested() @Type(() => AddressDto) address: AddressDto;

Array Validation

Validate array items:

import { IsArray, ArrayMinSize, ArrayMaxSize, ValidateNested } from "class-validator"; @Column({ type: "jsonb" }) @ApiPropertyDescribe({ type: EApiPropertyDescribeType.OBJECT, description: "User tags", isArray: true, }) @IsArray() @ArrayMinSize(1) @ArrayMaxSize(10) tags: string[]; // Validate nested objects in array @OneToMany(() => PhoneEntity, phone => phone.user) @ValidateNested({ each: true }) @Type(() => PhoneEntity) phones: PhoneEntity[];

Validation Groups

Use validation groups for different scenarios:

dto/update-user.dto.ts
import { IsNotEmpty } from "class-validator"; export class UpdateUserDto { @IsNotEmpty({ groups: ["admin"] }) role?: string; @IsNotEmpty({ groups: ["user", "admin"] }) email?: string; }

Error Messages

Customize validation error messages:

@Column() @ApiPropertyDescribe({ type: EApiPropertyDescribeType.STRING, description: "Username", format: EApiPropertyStringType.STRING, minLength: 3, maxLength: 20, }) @MinLength(3, { message: "Username is too short. Minimum length is 3 characters." }) @MaxLength(20, { message: "Username is too long. Maximum length is 20 characters." }) username: string;

Validation Pipes

Configure validation behavior globally:

main.ts
import { ValidationPipe } from "@nestjs/common"; import { NestFactory } from "@nestjs/core"; import { AppModule } from "./app.module"; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes( new ValidationPipe({ whitelist: true, // Strip properties not in DTO forbidNonWhitelisted: true, // Throw error on unknown properties transform: true, // Automatically transform payloads transformOptions: { enableImplicitConversion: true, }, }) ); await app.listen(3000); } bootstrap();

Validation in Subscribers

Perform custom validation in subscribers:

user-validation.subscriber.ts
import { Injectable, BadRequestException } from "@nestjs/common"; import { ApiRouteSubscriber, ApiRouteSubscriberBase, IApiSubscriberRouteExecutionContext } from "@elsikora/nestjs-crud-automator"; import { UserEntity } from "./user.entity"; @Injectable() @ApiRouteSubscriber({ entity: UserEntity, priority: 10 }) export class UserValidationSubscriber extends ApiRouteSubscriberBase<UserEntity> { async onBeforeCreate( context: IApiSubscriberRouteExecutionContext<UserEntity, any> ): Promise<any> { const { body } = context.result; // Custom validation logic if (body.email && body.email.endsWith("@blocked-domain.com")) { throw new BadRequestException("Email domain is not allowed"); } return context.result; } }

Next Steps

Last updated on