Exercices — Module 05
Validation, Pipes & DTOs · 5 exercices pratiques
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;
}
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,
) {}
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 });
}
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;
}
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;
}