Валидация на границах системы

Валидация входных данных на границах системы представляет собой ключевой механизм защиты прикладной логики от некорректных, неполных или злонамеренно сформированных данных. Любая система, взаимодействующая с внешним миром — HTTP API, очередь сообщений, файловые импортеры, WebSocket-события — неизбежно получает данные, которым нельзя доверять. Принцип «доверяй, но проверяй» в современных архитектурах заменяется на строгую обратную модель: недоверие ко всем входящим данным по умолчанию.

Библиотека class-validator предоставляет декларативный способ описания правил валидации через классы и декораторы, позволяя формализовать контракт входных данных и отделить его от бизнес-логики. На границах системы она становится фильтром, который отсекает некорректные структуры до их попадания в доменную область.


Граница системы — это любое место, где внешние данные переходят во внутренний контур приложения. В типичной архитектуре такими точками являются:

  • HTTP-контроллеры
  • RPC-методы (gRPC, message brokers)
  • обработчики очередей сообщений
  • импорт файлов (CSV, JSON, XML)
  • интеграции со сторонними API

На этих уровнях данные имеют внешний источник происхождения и могут нарушать внутренние инварианты системы. Основная задача валидации — привести входные данные к строго определённой структуре или отклонить их до дальнейшей обработки.

Использование class-validator в связке с class-transformer позволяет формировать промежуточные объекты (DTO), которые становятся формальным контрактом между внешним миром и доменной логикой.


Модель 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;
}

Преобразование выполняется до валидации, обеспечивая корректность типов на момент применения правил.


Строгая типизация и предотвращение «грязных» данных

Одной из ключевых проблем систем без строгой валидации является проникновение «грязных» данных:

  • строки вместо чисел
  • null вместо объектов
  • лишние поля
  • вложенные структуры неправильной формы

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

Корректное разделение ответственности при валидации на границе системы выглядит следующим образом:

  • транспортный слой: получение и первичная десериализация данных
  • слой валидации: проверка структуры и ограничений
  • слой трансформации: приведение типов
  • доменный слой: бизнес-логика без проверок входных данных

Нарушение этого разделения приводит к дублированию логики и снижению предсказуемости системы.


Антипаттерны использования

Некорректное применение валидации на границе системы приводит к ряду проблем:

  • частичная валидация внутри бизнес-логики вместо централизованной проверки
  • отсутствие преобразования типов при работе с внешними данными
  • использование DTO как доменных моделей
  • игнорирование рекурсивной валидации вложенных структур
  • смешивание валидации и побочных эффектов (например, обращение к базе внутри DTO без необходимости)

Такие подходы разрушают концепцию предсказуемой границы между внешним и внутренним контуром системы.


Контроль целостности данных в распределённых системах

В микросервисной архитектуре каждая граница сервиса становится точкой потенциальной неконсистентности. Данные, передаваемые между сервисами, должны проходить повторную валидацию независимо от источника.

Использование class-validator в каждом сервисе позволяет:

  • унифицировать правила валидации
  • снизить зависимость от доверия к внешним сервисам
  • предотвратить распространение некорректных данных по цепочке вызовов

Формализация контрактов взаимодействия

Валидация на границе системы фактически превращается в декларацию контракта:

  • структура входных данных фиксирована
  • допустимые диапазоны значений определены
  • правила расширяемости контролируются
  • нарушения фиксируются до попадания в бизнес-логику

class-validator выступает инструментом, который делает этот контракт явным и исполняемым на уровне кода.