Exercices — Module 05

Validation, Pipes & DTOs · 5 exercices pratiques

Ex 01 ⭐ Facile

DTO de création avec validations

Créez un CreateProductDto complet avec class-validator. Validez : name (string, 2-255 chars), sku (alphanumérique), price (nombre positif), stock (entier ≥ 0), description (optionnel, max 2000 chars).

Voir la solution
import {
  IsString, MinLength, MaxLength, Matches,
  IsNumber, IsPositive, IsInt, Min, IsOptional,
  IsBoolean, IsUrl,
} from 'class-validator';
import { Transform, Type } from 'class-transformer';

export class CreateProductDto {
  @IsString()
  @MinLength(2)
  @MaxLength(255)
  @Transform(({ value }) => value?.trim())
  name: string;

  @IsString()
  @Matches(/^[A-Z0-9-_]+$/, { message: 'SKU must be uppercase alphanumeric with - and _' })
  @MaxLength(50)
  sku: string;

  @IsNumber({ maxDecimalPlaces: 2 })
  @IsPositive({ message: 'Price must be positive' })
  @Type(() => Number)
  price: number;

  @IsInt()
  @Min(0)
  @Type(() => Number)
  stock: number;

  @IsOptional()
  @IsString()
  @MaxLength(2000)
  description?: string;

  @IsOptional()
  @IsUrl()
  imageUrl?: string;

  @IsOptional()
  @IsBoolean()
  isAvailable?: boolean;
}
Ex 02 ⭐ Facile

Mapped Types

À partir du CreateProductDto, créez UpdateProductDto (tous les champs optionnels), ProductQueryDto (pour la recherche) et CreateProductWithCategoryDto (qui ajoute categoryId).

Voir la solution
import { PartialType, IntersectionType, OmitType } from '@nestjs/mapped-types';

// Tous les champs de Create deviennent optionnels
export class UpdateProductDto extends PartialType(CreateProductDto) {}

// DTO pour query params
export class ProductQueryDto {
  @IsOptional() @IsString() q?: string;
  @IsOptional() @IsInt() @Min(1) @Type(() => Number) page?: number;
  @IsOptional() @IsInt() @Min(1) @Max(100) @Type(() => Number) limit?: number;
  @IsOptional() @IsNumber() @Type(() => Number) minPrice?: number;
  @IsOptional() @IsNumber() @Type(() => Number) maxPrice?: number;
  @IsOptional() @IsInt() @Type(() => Number) categoryId?: number;
}

// Ajouter categoryId requis
class WithCategoryDto {
  @IsInt() @IsPositive() @Type(() => Number)
  categoryId: number;
}
export class CreateProductWithCategoryDto extends IntersectionType(
  CreateProductDto,
  WithCategoryDto,
) {}
Ex 03 ⭐⭐ Moyen

Pipe personnalisé de transformation

Créez un pipe SlugifyPipe qui transforme une chaîne en slug URL-friendly (minuscules, espaces → tirets, caractères spéciaux supprimés). Appliquez-le au champ name d'un DTO.

Voir la solution
@Injectable()
export class SlugifyPipe implements PipeTransform<string, string> {
  transform(value: string): string {
    return value
      .toLowerCase()
      .trim()
      .normalize('NFD')
      .replace(/[̀-ͯ]/g, '')  // Supprimer les accents
      .replace(/[^a-z0-9\s-]/g, '')     // Garder lettres, chiffres, espaces, tirets
      .replace(/[\s_-]+/g, '-')         // Espaces/underscores → tirets
      .replace(/^-+|-+$/g, '');         // Supprimer tirets en début/fin
  }
}

// Usage dans un controller
@Post()
create(
  @Body('name', SlugifyPipe) slug: string,
  @Body() dto: CreateCategoryDto,
) {
  return this.categoriesService.create({ ...dto, slug });
}
Ex 04 ⭐⭐ Moyen

Validation conditionnelle

Créez un DTO de paiement avec validation conditionnelle : si paymentMethod === 'card', les champs cardNumber et cvv sont requis. Si paymentMethod === 'paypal', seul paypalEmail est requis.

Voir la solution
export class PaymentDto {
  @IsEnum(['card', 'paypal', 'bank_transfer'])
  paymentMethod: 'card' | 'paypal' | 'bank_transfer';

  // Requis uniquement si paiement par carte
  @ValidateIf(o => o.paymentMethod === 'card')
  @IsString()
  @Matches(/^\d{16}$/, { message: 'Card number must be 16 digits' })
  cardNumber?: string;

  @ValidateIf(o => o.paymentMethod === 'card')
  @IsString()
  @Matches(/^\d{3,4}$/, { message: 'CVV must be 3 or 4 digits' })
  cvv?: string;

  @ValidateIf(o => o.paymentMethod === 'card')
  @IsDateString()
  expiryDate?: string;

  // Requis uniquement si paiement PayPal
  @ValidateIf(o => o.paymentMethod === 'paypal')
  @IsEmail()
  paypalEmail?: string;

  @IsNumber()
  @IsPositive()
  amount: number;
}
Ex 05 ⭐⭐⭐ Difficile

Décorateur de validation personnalisé

Créez un décorateur de validation @IsStrongPassword() avec registerDecorator qui vérifie : min 8 chars, au moins une majuscule, une minuscule, un chiffre, un caractère spécial. Incluez un message d'erreur personnalisé.

Voir la solution
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';

export function IsStrongPassword(options?: ValidationOptions) {
  return function(object: object, propertyName: string) {
    registerDecorator({
      name: 'isStrongPassword',
      target: object.constructor,
      propertyName,
      options: {
        message: `${propertyName} must have min 8 chars, uppercase, lowercase, digit, and special char`,
        ...options,
      },
      validator: {
        validate(value: any, _args: ValidationArguments) {
          if (typeof value !== 'string') return false;
          return (
            value.length >= 8 &&
            /[A-Z]/.test(value) &&
            /[a-z]/.test(value) &&
            /\d/.test(value) &&
            /[!@#$%^&*()_+\-=\[\]{};':"\\|,. <>\/?]/.test(value)
          );
        },
      },
    });
  };
}

// Usage dans le DTO
export class RegisterDto {
  @IsEmail() email: string;
  @IsStrongPassword() password: string;
}
← Cours ▶ Mini-projet Module 06 →