DTO и их валидация

DTO (Data Transfer Object) в экосистеме JavaScript и TypeScript представляет собой структурированную модель данных, предназначенную для передачи информации между слоями приложения. В контексте серверной разработки DTO выступает формальным контрактом входящих и исходящих данных, определяя форму объекта до того, как он попадёт в бизнес-логику.

Основная задача DTO заключается в изоляции внутренней модели системы от внешних запросов. Любой входящий payload рассматривается как потенциально некорректный или неполный, поэтому требуется явное описание структуры, типов и ограничений.

В связке с библиотекой class-validator DTO приобретает поведенческую составляющую: становится не просто структурой, а объектом с правилами проверки.


Базовая модель валидации через class-validator

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

Пример минимального DTO:

import { IsString, IsInt } fr om 'class-validator';

export class CreateUserDto {
  @IsString()
  username: string;

  @IsInt()
  age: number;
}

Каждое поле снабжается набором ограничений, которые проверяются во время выполнения. Валидация осуществляется через функцию validate:

import { validate } from 'class-validator';

const dto = new CreateUserDto();
dto.username = 'alex';
dto.age = 25;

const errors = await validate(dto);

Результатом выполнения становится массив ошибок. Пустой массив означает прохождение всех проверок.


Преобразование входящих данных в DTO

JSON-запросы не содержат экземпляров классов. Они представляют собой обычные объекты. Для применения class-validator требуется преобразование plain object в class instance.

Используется class-transformer:

import { plainToInstance } from 'class-transformer';

const dto = plainToInstance(CreateUserDto, requestBody);

Только после этого становятся доступными декораторы class-validator, поскольку они работают с метаданными классов.


Типизация и ограничения базовых полей

Набор стандартных декораторов class-validator охватывает большинство типовых проверок.

Строки

import { IsString, Length, IsNotEmpty } from 'class-validator';

export class UserDto {
  @IsString()
  @IsNotEmpty()
  @Length(3, 20)
  username: string;
}

Основные ограничения:

  • IsString — проверка строкового типа
  • IsNotEmpty — запрет пустых значений
  • Length — диапазон длины

Числа

import { IsInt, Min, Max } from 'class-validator';

export class AgeDto {
  @IsInt()
  @Min(0)
  @Max(120)
  age: number;
}

Числовая валидация часто используется для ограничений бизнес-уровня, но может быть вынесена в DTO для раннего отсечения некорректных данных.


Булевы и перечисления

Булевы значения

import { IsBoolean } from 'class-validator';

export class FlagDto {
  @IsBoolean()
  isActive: boolean;
}

Перечисления

import { IsEnum } from 'class-validator';

enum Role {
  USER = 'user',
  ADMIN = 'admin',
}

export class RoleDto {
  @IsEnum(Role)
  role: Role;
}

Enum-валидация фиксирует допустимое множество значений и предотвращает появление произвольных строк.


Обязательность и опциональные поля

DTO часто описывает частично заполняемые структуры. В таких случаях применяется IsOptional.

import { IsOptional, IsString } from 'class-validator';

export class UpdateUserDto {
  @IsOptional()
  @IsString()
  username?: string;
}

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


Вложенные DTO и рекурсивная валидация

Сложные структуры требуют вложенных объектов. Для корректной валидации применяется комбинация ValidateNested и Type.

import { ValidateNested, IsString } from 'class-validator';
import { Type } from 'class-transformer';

class AddressDto {
  @IsString()
  city: string;

  @IsString()
  street: string;
}

export class UserWithAddressDto {
  @IsString()
  name: string;

  @ValidateNested()
  @Type(() => AddressDto)
  address: AddressDto;
}

ValidateNested активирует проверку внутреннего объекта, а Type обеспечивает корректное преобразование plain object в экземпляр класса.


Валидация массивов

Работа с коллекциями требует комбинирования декораторов.

import { IsArray, IsString } from 'class-validator';

export class TagsDto {
  @IsArray()
  @IsString({ each: true })
  tags: string[];
}

Параметр each: true применяет правило ко всем элементам массива.


Преобразование типов входящих значений

HTTP-запросы передают данные в виде строк, даже если логически они являются числами или булевыми значениями. Для автоматической трансформации используется class-transformer.

import { Type } from 'class-transformer';
import { IsInt } from 'class-validator';

export class PaginationDto {
  @Type(() => Number)
  @IsInt()
  page: number;

  @Type(() => Number)
  @IsInt()
  lim it: number;
}

Без трансформации значения "10" и "20" останутся строками и провалят проверку IsInt.


Группы валидации

Группы позволяют применять разные правила в зависимости от сценария использования DTO.

import { IsString } from 'class-validator';

export class UserDto {
  @IsString({ groups: ['create'] })
  username: string;

  @IsString({ groups: ['update'] })
  nickname: string;
}

При вызове валидации указывается группа:

validate(dto, { groups: ['create'] });

Это позволяет использовать один класс для разных операций.


Кастомные валидаторы

Стандартных проверок недостаточно для сложной бизнес-логики. class-validator поддерживает создание пользовательских декораторов.

import {
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments,
  registerDecorator,
} from 'class-validator';

@ValidatorConstraint({ async: false })
class IsEvenConstraint implements ValidatorConstraintInterface {
  validate(value: number) {
    return value % 2 === 0;
  }

  defaultMessage(args: ValidationArguments) {
    return `${args.property} должно быть чётным числом`;
  }
}

export function IsEven() {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      validator: IsEvenConstraint,
    });
  };
}

Использование:

export class NumberDto {
  @IsEven()
  value: number;
}

Поведение при ошибках валидации

Результат validate представляет собой массив объектов ошибок. Каждый объект содержит:

  • имя поля
  • ограничения, которые не выполнены
  • вложенные ошибки (при наличии nested DTO)

Типичная структура:

[
  {
    property: 'username',
    constraints: {
      isString: 'username must be a string',
    },
  },
]

При интеграции в серверный фреймворк этот массив часто преобразуется в HTTP-ответ с кодом 400.


Строгая фильтрация входных данных

В системах, использующих DTO как границу входа, применяется дополнительная фильтрация:

  • whitelist: true — удаляет неизвестные поля
  • forbidNonWhitelisted: true — вызывает ошибку при наличии лишних полей
  • transform: true — автоматически преобразует payload в DTO

Эти параметры формируют строгий контракт данных, исключающий неописанные свойства.


Поведение при работе с null и undefined

class-validator различает отсутствие значения и явное значение null.

  • undefined часто обрабатывается через IsOptional
  • null требует явной обработки через IsDefined или кастомные правила
import { IsDefined, IsString } from 'class-validator';

export class StrictDto {
  @IsDefined()
  @IsString()
  field: string;
}

Совместное использование с архитектурой приложения

DTO обычно располагаются на границе контроллера и сервисного слоя. Их задача заключается в том, чтобы:

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

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


Поведение производительности и особенности метаданных

class-validator использует reflect-metadata для хранения информации о декораторах. Это означает:

  • необходимость включения experimentalDecorators и emitDecoratorMetadata в TypeScript
  • наличие runtime-накладных расходов на проверку
  • важность ограничения глубины вложенных структур для сложных DTO

Валидация больших графов объектов может стать затратной операцией, особенно при рекурсивных структурах.


Типичные ошибки при проектировании DTO

Часто встречающиеся проблемы:

  • отсутствие plainToInstance, из-за чего декораторы не срабатывают
  • неправильный порядок декораторов (IsOptional должен идти первым)
  • использование DTO как доменной модели
  • отсутствие each: true для массивов
  • игнорирование трансформации типов

Эти ошибки приводят к тому, что валидация формально присутствует, но фактически не выполняется.


Расширение через композицию DTO

DTO удобно комбинировать, переиспользуя общие части:

class BaseUserDto {
  @IsString()
  name: string;
}

class ExtendedUserDto extends BaseUserDto {
  @IsString()
  role: string;
}

Такой подход снижает дублирование и упрощает поддержку контрактов данных при росте системы.