Валидация входных данных на границах системы представляет собой ключевой механизм защиты прикладной логики от некорректных, неполных или злонамеренно сформированных данных. Любая система, взаимодействующая с внешним миром — HTTP API, очередь сообщений, файловые импортеры, WebSocket-события — неизбежно получает данные, которым нельзя доверять. Принцип «доверяй, но проверяй» в современных архитектурах заменяется на строгую обратную модель: недоверие ко всем входящим данным по умолчанию.
Библиотека class-validator предоставляет декларативный
способ описания правил валидации через классы и декораторы, позволяя
формализовать контракт входных данных и отделить его от бизнес-логики.
На границах системы она становится фильтром, который отсекает
некорректные структуры до их попадания в доменную область.
Граница системы — это любое место, где внешние данные переходят во внутренний контур приложения. В типичной архитектуре такими точками являются:
На этих уровнях данные имеют внешний источник происхождения и могут нарушать внутренние инварианты системы. Основная задача валидации — привести входные данные к строго определённой структуре или отклонить их до дальнейшей обработки.
Использование class-validator в связке с
class-transformer позволяет формировать промежуточные
объекты (DTO), которые становятся формальным контрактом между внешним
миром и доменной логикой.
DTO (Data Transfer Object) выступает в роли описания структуры
входящего запроса. В связке с class-validator DTO
превращается в самодокументируемую схему с правилами валидации.
import { IsString, IsInt, Min, Max } from 'class-validator';
export class CreateUserDto {
@IsString()
username: string;
@IsInt()
@Min(0)
@Max(120)
age: number;
}
Каждое поле описывает одновременно тип данных и ограничения. Важно, что валидация не привязана к транспортному уровню, а является частью модели данных, что делает её повторно используемой в различных сценариях входа.
Ключевая задача валидации на границе — не допустить попадания некорректных данных в доменный слой. Это достигается за счёт:
Пример: вместо проверки
if (!user.age || user.age < 0) внутри сервиса, правило
фиксируется в DTO и гарантируется системой валидации до вызова
бизнес-метода.
В JavaScript и TypeScript данные, поступающие извне, часто имеют строковый тип, даже если по смыслу являются числами или булевыми значениями. Например, HTTP-запросы передают все query-параметры как строки.
Для решения этой проблемы используется преобразование с
class-transformer:
import { Type } from 'class-transformer';
import { IsInt } from 'class-validator';
export class QueryDto {
@Type(() => Number)
@IsInt()
page: number;
}
Преобразование выполняется до валидации, обеспечивая корректность типов на момент применения правил.
Одной из ключевых проблем систем без строгой валидации является проникновение «грязных» данных:
class-validator позволяет формализовать строгие
правила:
import { IsEmail, IsNotEmpty, IsOptional } from 'class-validator';
export class UpdateProfileDto {
@IsOptional()
@IsEmail()
email?: string;
@IsNotEmpty()
displayName: string;
}
Таким образом достигается контроль как обязательных, так и опциональных полей.
Сложные структуры данных часто содержат вложенные сущности. Без рекурсивной валидации такие структуры становятся источником скрытых ошибок.
import { ValidateNested, IsString } from 'class-validator';
import { Type } from 'class-transformer';
class AddressDto {
@IsString()
city: string;
@IsString()
street: string;
}
export class UserDto {
@IsString()
name: string;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}
Декоратор @ValidateNested() активирует рекурсивную
проверку вложенного объекта, а @Type() обеспечивает
корректное преобразование.
Массивы требуют отдельного подхода, поскольку валидация должна применяться к каждому элементу.
import { IsArray, IsString, ArrayMinSize } from 'class-validator';
export class TagsDto {
@IsArray()
@ArrayMinSize(1)
@IsString({ each: true })
tags: string[];
}
Флаг each: true задаёт применение правила ко всем
элементам массива, обеспечивая целостность структуры.
Стандартного набора декораторов часто недостаточно для предметных правил. В таких случаях используются пользовательские валидаторы.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
@ValidatorConstraint({ name: 'isEven', async: false })
export class IsEvenConstraint implements ValidatorConstraintInterface {
validate(value: number) {
return value % 2 === 0;
}
defaultMessage(args: ValidationArguments) {
return `${args.property} должно быть чётным числом`;
}
}
Использование:
import { Validate } from 'class-validator';
export class NumberDto {
@Validate(IsEvenConstraint)
value: number;
}
Пользовательские валидаторы позволяют формализовать доменные ограничения, не смешивая их с бизнес-логикой.
Некоторые правила требуют обращения к внешним системам: база данных,
API, кэш. class-validator поддерживает асинхронные
проверки.
@ValidatorConstraint({ async: true })
export class IsUserExists implements ValidatorConstraintInterface {
async validate(id: number) {
const user = await userRepository.findById(id);
return Boolean(user);
}
}
Асинхронные валидаторы особенно важны на границе системы, где необходимо проверять существование сущностей или уникальность значений до выполнения операций.
Помимо проверки значений, важной задачей является устранение лишних полей и приведение структуры к строго заданной форме.
В связке с class-validator часто применяется стратегия
whitelist:
{
whitelist: true,
forbidNonWhitelisted: true
}
Эта конфигурация обеспечивает:
Результатом работы валидатора является массив ошибок, содержащих структуру:
Типичная форма обработки включает преобразование ошибок в унифицированный формат ответа API.
Ошибки валидации должны рассматриваться как ожидаемый результат работы границы системы, а не как исключительные ситуации уровня инфраструктуры.
Корректное разделение ответственности при валидации на границе системы выглядит следующим образом:
Нарушение этого разделения приводит к дублированию логики и снижению предсказуемости системы.
Некорректное применение валидации на границе системы приводит к ряду проблем:
Такие подходы разрушают концепцию предсказуемой границы между внешним и внутренним контуром системы.
В микросервисной архитектуре каждая граница сервиса становится точкой потенциальной неконсистентности. Данные, передаваемые между сервисами, должны проходить повторную валидацию независимо от источника.
Использование class-validator в каждом сервисе
позволяет:
Валидация на границе системы фактически превращается в декларацию контракта:
class-validator выступает инструментом, который делает
этот контракт явным и исполняемым на уровне кода.