Validation des données entrantes
Valider les données à la frontière de l'application
Ne jamais faire confiance aux données venant du client. Les DTOs (Data Transfer Objects) + class-validator + ValidationPipe forment un mur de validation solide et déclaratif.
npm install class-validator class-transformer
DTOs — Data Transfer Objects
Un DTO est une classe qui décrit la forme des données attendues pour une opération spécifique.
// create-user.dto.ts
import { IsEmail, IsString, MinLength, MaxLength, IsOptional, IsEnum } from 'class-validator';
import { Transform } from 'class-transformer';
export enum UserRole {
USER = 'user',
ADMIN = 'admin',
}
export class CreateUserDto {
@IsString({ message: 'Le nom doit être une chaîne' })
@MinLength(2, { message: 'Le nom doit faire au moins 2 caractères' })
@MaxLength(100)
@Transform(({ value }) => value?.trim())
name: string;
@IsEmail({}, { message: 'Email invalide' })
@Transform(({ value }) => value?.toLowerCase())
email: string;
@IsString()
@MinLength(8, { message: 'Le mot de passe doit faire au moins 8 caractères' })
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: 'Le mot de passe doit contenir une majuscule, une minuscule et un chiffre',
})
password: string;
@IsOptional()
@IsEnum(UserRole)
role?: UserRole;
}
class-validator — décorateurs de validation
Décorateurs courants
// Chaînes
@IsString()
@IsNotEmpty()
@MinLength(3)
@MaxLength(255)
@Matches(/^[a-zA-Z]+$/, { message: 'Lettres uniquement' })
@IsUrl()
@IsEmail()
@IsUUID()
// Nombres
@IsNumber()
@IsInt()
@Min(0)
@Max(100)
@IsPositive()
// Booléens
@IsBoolean()
// Dates
@IsDate()
@IsDateString()
// Tableaux
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(10)
@ArrayUnique()
// Objets imbriqués
@IsObject()
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
// Conditions
@IsOptional() // OK si absent ou null/undefined
@ValidateIf(o => o.type === 'premium') // Seulement si condition vraie
premiumFeature: string;
Validation d'objets imbriqués
export class AddressDto {
@IsString() @IsNotEmpty() street: string;
@IsString() @IsNotEmpty() city: string;
@IsString() @Length(5, 5) zipCode: string;
@IsString() @IsNotEmpty() country: string;
}
export class CreateOrderDto {
@ValidateNested()
@Type(() => AddressDto) // Requis pour la transformation
shippingAddress: AddressDto;
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
items: OrderItemDto[];
}
Pipes NestJS
Un pipe transforme ou valide les données avant qu'elles n'atteignent le handler. NestJS inclut plusieurs pipes built-in.
Pipes built-in
// ParseIntPipe — convertit string en number
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) { ... }
// ParseUUIDPipe — valide et parse un UUID
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) { ... }
// ParseBoolPipe — convertit 'true'/'false' en boolean
@Get()
findAll(@Query('active', ParseBoolPipe) active: boolean) { ... }
// ParseArrayPipe — parse un tableau JSON
@Get()
findByIds(@Query('ids', new ParseArrayPipe({ items: Number })) ids: number[]) { ... }
// ParseEnumPipe — valide une valeur d'énumération
@Get()
findByRole(@Param('role', new ParseEnumPipe(UserRole)) role: UserRole) { ... }
// DefaultValuePipe — valeur par défaut
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
) { ... }
Scope des pipes
// Niveau paramètre (le plus précis)
@Param('id', ParseIntPipe)
// Niveau méthode
@UsePipes(new ValidationPipe())
@Post()
create(@Body() dto: CreateUserDto) { ... }
// Niveau controller
@Controller('users')
@UsePipes(new ValidationPipe({ transform: true }))
export class UsersController { ... }
// Niveau global (recommandé)
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
ValidationPipe — configuration complète
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Supprime les propriétés non déclarées dans le DTO
forbidNonWhitelisted: true,// Erreur 400 si champ inconnu envoyé
transform: true, // Transforme les types automatiquement (string '42' → number 42)
transformOptions: {
enableImplicitConversion: true, // Conversion implicite des types primitifs
},
disableErrorMessages: false,
validationError: {
target: false, // N'inclut pas l'objet cible dans l'erreur
value: false, // N'inclut pas la valeur dans l'erreur
},
exceptionFactory: (errors) => {
const messages = errors.map(err =>
Object.values(err.constraints || {}).join(', ')
);
return new BadRequestException({
statusCode: 400,
message: messages,
error: 'Validation failed',
});
},
}),
);
Pipes personnalisés
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class PositiveIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val) || val < 1) {
throw new BadRequestException(`"${value}" n'est pas un entier positif`);
}
return val;
}
}
// Pipe de transformation
@Injectable()
export class TrimPipe implements PipeTransform {
transform(value: any) {
if (typeof value === 'string') return value.trim();
if (typeof value === 'object' && value !== null) {
return Object.fromEntries(
Object.entries(value).map(([k, v]) => [k, typeof v === 'string' ? v.trim() : v])
);
}
return value;
}
}
// Usage
@Get(':id')
findOne(@Param('id', PositiveIntPipe) id: number) { ... }
Mapped Types
@nestjs/mapped-types permet de créer des DTOs dérivés sans dupliquer le code.
import { PartialType, OmitType, PickType, IntersectionType } from '@nestjs/mapped-types';
// PartialType — tous les champs optionnels
export class UpdateUserDto extends PartialType(CreateUserDto) {}
// OmitType — exclure des champs
export class CreateUserWithoutPasswordDto extends OmitType(CreateUserDto, ['password'] as const) {}
// PickType — ne garder que certains champs
export class LoginDto extends PickType(CreateUserDto, ['email', 'password'] as const) {}
// IntersectionType — combiner deux DTOs
export class CreateUserWithAddressDto extends IntersectionType(
CreateUserDto,
AddressDto,
) {}
// Composition avancée
export class UpdateProfileDto extends PartialType(
OmitType(CreateUserDto, ['password', 'email'] as const)
) {}
Sérialisation avec class-transformer
import { Exclude, Expose, Transform, Type } from 'class-transformer';
import { UseInterceptors, ClassSerializerInterceptor } from '@nestjs/common';
export class UserResponseDto {
@Expose()
id: number;
@Expose()
name: string;
@Expose()
email: string;
@Exclude() // Ne jamais exposer le mot de passe !
password: string;
@Expose()
@Transform(({ value }) => value?.toUpperCase())
role: string;
@Expose()
@Type(() => PostSummaryDto)
posts: PostSummaryDto[];
constructor(partial: Partial<UserResponseDto>) {
Object.assign(this, partial);
}
}
// Dans le controller
@Controller('users')
@UseInterceptors(ClassSerializerInterceptor)
export class UsersController {
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
const user = await this.usersService.findOne(id);
return new UserResponseDto(user); // Applique les décorateurs
}
}
Bonne pratique : Utilisez des DTOs de réponse séparés plutôt que de retourner directement les entités TypeORM. Cela protège vos données sensibles et découple votre API de la structure de la base de données.