NestJS Enterprise Backend APIs · Lezione

Validatori personalizzati e vincoli asincroni

Scriva regole riutilizzabili con @ValidatorConstraint, inclusi controlli asincroni sul database.

Lezione 2 di 413 passaggi

Validatori personalizzati e vincoli asincroni è una lezione NestJS Enterprise Backend APIs gratuita su CoddyKit. Questa è la lezione 2 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento NestJS Enterprise Backend APIs, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso NestJS Enterprise Backend APIs include 4 lezioni in totale.

Parti di questa lezione non sono ancora state tradotte e vengono mostrate in inglese.

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) returns boolean (or Promise<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.

  • registerDecorator binds your constraint class to a target property.
  • validationOptions lets 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: true in @ValidatorConstraint.
  • Return a Promise<boolean> from validate.
  • 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 }) in main.ts.
  • Register the constraint as a provider in the module that owns the repository.
  • fallbackOnErrors: true lets 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 IsEmailUniqueConstraint in providers.
  • If other modules build DTOs using this rule, export it.
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 real UNIQUE constraint.
  • 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 ValidatorConstraintInterface with validate and defaultMessage, decorated by @ValidatorConstraint({ name, async }).
  • Wrap it in a factory using registerDecorator for a clean @MyRule() decorator, passing parameters via the constraints array.
  • For async DB checks, make the constraint @Injectable(), register it as a provider, and call useContainer(...) in main.ts so DI works.
  • Remember the TOCTOU caveat: async uniqueness validators improve UX but must be backed by a database UNIQUE constraint for true integrity.
Gratis per iniziare

Impara TypeScript con un tutor IA — gratis

Scrivi ed esegui vero codice nel tuo browser, ricevi aiuto istantaneo da un tutor IA disponibile 24/7, e riprendi da dove hai lasciato sul web o nell'app.

Corsi
20
Lezioni
76

Domande Frequenti

La lezione «Validatori personalizzati e vincoli asincroni» è gratuita?

Sì — il testo completo di «Validatori personalizzati e vincoli asincroni» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso NestJS Enterprise Backend APIs, passa a CoddyKit PRO. Il corso NestJS Enterprise Backend APIs include 4 lezioni in totale.

Cosa imparerò in «Validatori personalizzati e vincoli asincroni»?

Scriva regole riutilizzabili con @ValidatorConstraint, inclusi controlli asincroni sul database. Eserciti NestJS Enterprise Backend APIs con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare NestJS Enterprise Backend APIs?

Non è richiesta alcuna esperienza precedente. NestJS Enterprise Backend APIs su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 2 di 4.

Quanto tempo richiede la lezione «Validatori personalizzati e vincoli asincroni»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione NestJS Enterprise Backend APIs?

Sì. Ogni lezione NestJS Enterprise Backend APIs include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Validazione di DTO annidati e array
  2. Validatori personalizzati e vincoli asincroni
  3. Modellazione delle risposte con ClassSerializerInterceptor
  4. Validazione condizionale e gruppi dinamici
← Torna a NestJS Enterprise Backend APIs