Validateurs personnalisés et contraintes asynchrones
Écrivez des règles @ValidatorConstraint réutilisables, notamment des vérifications asynchrones dans la base de données.
Validateurs personnalisés et contraintes asynchrones est une leçon NestJS Enterprise Backend APIs gratuite sur CoddyKit. Ceci est la leçon 2 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage NestJS Enterprise Backend APIs, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours NestJS Enterprise Backend APIs comprend 4 leçons au total.
Certaines parties de cette leçon n'ont pas encore été traduites et s'affichent en anglais.
Why Custom Validators?
The class-validator decorators that NestJS ships with (@IsEmail, @Min, @Length) cover generic shapes, but enterprise rules are domain-specific: "this coupon code must exist and not be expired" or "this email must be unique in the users table".
- Custom validators let you encapsulate such rules behind a single reusable decorator.
- They keep DTOs declarative and your business logic out of controllers.
- Two flavors exist: inline (
@Validate/registerDecorator) and constraint classes (@ValidatorConstraint).
In this lesson we focus on the @ValidatorConstraint class approach, including async checks that hit a database.
Anatomy of a ValidatorConstraint
A constraint class implements ValidatorConstraintInterface and is decorated with @ValidatorConstraint. It exposes two methods:
validate(value, args)returnsboolean(orPromise<boolean>for async).defaultMessage(args)returns the error string when validation fails.
The name option is the rule identifier, and async: true tells class-validator to await the result.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
@ValidatorConstraint({ name: 'isStrongPassword', async: false })
export class IsStrongPasswordConstraint
implements ValidatorConstraintInterface
{
validate(value: string, _args: ValidationArguments): boolean {
if (typeof value !== 'string') return false;
const hasUpper = /[A-Z]/.test(value);
const hasDigit = /[0-9]/.test(value);
return value.length >= 8 && hasUpper && hasDigit;
}
defaultMessage(args: ValidationArguments): string {
return `${args.property} must be 8+ chars with an uppercase letter and a digit`;
}
}Wrapping It in a Decorator
Implementing the constraint is only half the story. To get a clean @IsStrongPassword() decorator you wrap registerDecorator in a factory function.
registerDecoratorbinds your constraint class to a target property.validationOptionslets callers override the message per-field.- The factory returns a
PropertyDecorator, so it reads like a built-in decorator on the DTO.
import { registerDecorator, ValidationOptions } from 'class-validator';
import { IsStrongPasswordConstraint } from './is-strong-password.constraint';
export function IsStrongPassword(options?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName,
options,
constraints: [],
validator: IsStrongPasswordConstraint,
});
};
}
// Usage in a DTO:
// class CreateUserDto {
// @IsStrongPassword()
// password: string;
// }The Pure Logic Is Testable
The core of a validator is plain TypeScript with no framework coupling. You can extract and unit-test the predicate in isolation. Below is a standalone program demonstrating the strong-password rule.
function isStrongPassword(value: string): boolean {
if (typeof value !== 'string') return false;
const hasUpper = /[A-Z]/.test(value);
const hasDigit = /[0-9]/.test(value);
return value.length >= 8 && hasUpper && hasDigit;
}
const cases = ['abc', 'alllowercase1', 'Short1', 'GoodPass99'];
for (const c of cases) {
console.log(`${c.padEnd(15)} -> ${isStrongPassword(c)}`);
}Passing Arguments to a Constraint
Validators often need parameters: a minimum age, an allowed currency list, or a sibling field to compare against. You pass them via the constraints array, then read them inside validate through args.constraints.
A classic example is @Match, which checks one property equals another (e.g. passwordConfirm equals password).
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
@ValidatorConstraint({ name: 'match', async: false })
export class MatchConstraint implements ValidatorConstraintInterface {
validate(value: unknown, args: ValidationArguments): boolean {
const [relatedProperty] = args.constraints as [string];
const related = (args.object as Record<string, unknown>)[relatedProperty];
return value === related;
}
defaultMessage(args: ValidationArguments): string {
const [relatedProperty] = args.constraints as [string];
return `${args.property} must match ${relatedProperty}`;
}
}
export function Match(property: string, options?: ValidationOptions) {
return (object: object, propertyName: string) =>
registerDecorator({
target: object.constructor,
propertyName,
options,
constraints: [property],
validator: MatchConstraint,
});
}Going Async: Database Constraints
The real enterprise power is async validation: checking a value against the database during request validation. The most common case is uniqueness.
- Set
async: truein@ValidatorConstraint. - Return a
Promise<boolean>fromvalidate. - Inject a repository/service into the constraint class.
For dependency injection to work, the constraint must be injectable and class-validator must use Nest's container (covered in the next scene).
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
import { User } from './user.entity';
@ValidatorConstraint({ name: 'isEmailUnique', async: true })
@Injectable()
export class IsEmailUniqueConstraint
implements ValidatorConstraintInterface
{
constructor(
@InjectRepository(User) private readonly users: Repository<User>,
) {}
async validate(email: string): Promise<boolean> {
const existing = await this.users.findOne({ where: { email } });
return existing === null;
}
defaultMessage(args: ValidationArguments): string {
return `email '${args.value}' is already registered`;
}
}Wiring DI With useContainer
By default class-validator instantiates constraint classes itself, so @Injectable() dependencies are undefined. You must tell class-validator to resolve constraints through Nest's DI container.
- Call
useContainer(app.select(AppModule), { fallbackOnErrors: true })inmain.ts. - Register the constraint as a provider in the module that owns the repository.
fallbackOnErrors: truelets non-injectable built-in validators still work.
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { useContainer } from 'class-validator';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Resolve custom validators via Nest's DI container
useContainer(app.select(AppModule), { fallbackOnErrors: true });
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.listen(3000);
}
bootstrap();Registering the Constraint as a Provider
The injectable constraint only gets its dependencies if Nest knows about it. Add it to the module providers array alongside the feature it validates.
- Import
TypeOrmModule.forFeature([User])so the repository token is available. - List
IsEmailUniqueConstraintinproviders. - If other modules build DTOs using this rule,
exportit.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersService } from './users.service';
import { IsEmailUniqueConstraint } from './is-email-unique.constraint';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService, IsEmailUniqueConstraint],
exports: [IsEmailUniqueConstraint],
})
export class UsersModule {}Using the Async Decorator on a DTO
Once wired, the async rule reads exactly like a sync one. The global ValidationPipe awaits it automatically, so a duplicate email yields a 422/400 response before your controller ever runs.
Compose it with standard decorators — order does not matter, all run and collected errors are merged.
import { IsEmail, IsNotEmpty } from 'class-validator';
import { IsEmailUnique } from './is-email-unique.decorator';
import { IsStrongPassword } from './is-strong-password.decorator';
export class RegisterUserDto {
@IsEmail()
@IsEmailUnique({ message: 'This email is taken' })
email: string;
@IsNotEmpty()
@IsStrongPassword()
password: string;
}Performance and TOCTOU Caveats
Async DB validators are convenient but carry trade-offs you must design around in production:
- Extra query per request — each async rule is a round trip; avoid stacking many on hot endpoints.
- Race condition (TOCTOU) — between the validation read and the actual
INSERT, another request can claim the value. The check is not a substitute for a realUNIQUEconstraint. - Always keep a database-level unique index and catch the duplicate error as the final guard.
Treat async validators as a UX nicety that returns friendly field errors, not as the source of truth for integrity.
Returning Friendly, Targeted Errors
A well-built constraint produces messages tied to the failing field, which the ValidationPipe aggregates into a structured response. You can interpolate the value and property via ValidationArguments.
This standalone snippet simulates how a constraint's validate/defaultMessage pair would behave against a fake user store.
const existingEmails = new Set(['ada@corp.io', 'grace@corp.io']);
function validateUnique(email: string): { ok: boolean; message?: string } {
const ok = !existingEmails.has(email);
return ok ? { ok } : { ok, message: `email '${email}' is already registered` };
}
for (const email of ['ada@corp.io', 'linus@corp.io']) {
const result = validateUnique(email);
console.log(`${email.padEnd(16)} -> ${JSON.stringify(result)}`);
}Quick Check
You added an injectable @ValidatorConstraint({ async: true }) class that injects a TypeORM repository, but at runtime the repository is undefined and the app throws. What is the missing step?
Recap
You can now build reusable, framework-grade validators in NestJS:
- Implement
ValidatorConstraintInterfacewithvalidateanddefaultMessage, decorated by@ValidatorConstraint({ name, async }). - Wrap it in a factory using
registerDecoratorfor a clean@MyRule()decorator, passing parameters via theconstraintsarray. - For async DB checks, make the constraint
@Injectable(), register it as a provider, and calluseContainer(...)inmain.tsso DI works. - Remember the TOCTOU caveat: async uniqueness validators improve UX but must be backed by a database
UNIQUEconstraint for true integrity.
Questions Fréquemment Posées
La leçon « Validateurs personnalisés et contraintes asynchrones » est-elle gratuite ?
Oui — le texte complet de « Validateurs personnalisés et contraintes asynchrones » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours NestJS Enterprise Backend APIs, passe à CoddyKit PRO. Le cours NestJS Enterprise Backend APIs comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « Validateurs personnalisés et contraintes asynchrones » ?
Écrivez des règles @ValidatorConstraint réutilisables, notamment des vérifications asynchrones dans la base de données. Tu pratiques NestJS Enterprise Backend APIs avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.
Dois-je avoir de l'expérience pour commencer NestJS Enterprise Backend APIs ?
Aucune expérience préalable n'est requise. NestJS Enterprise Backend APIs sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 2 sur 4.
Combien de temps prend la leçon « Validateurs personnalisés et contraintes asynchrones » ?
La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.
Peux-tu écrire et exécuter du code dans cette leçon NestJS Enterprise Backend APIs ?
Oui. Chaque leçon NestJS Enterprise Backend APIs inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.
Toutes les leçons de ce cours
- Validation des DTO imbriqués et des tableaux
- Validateurs personnalisés et contraintes asynchrones
- Mise en forme des réponses avec ClassSerializerInterceptor
- Validation conditionnelle et groupes dynamiques