Валидация перед сохранением в базу

Валидация входных данных перед сохранением в базу данных рассматривается как отдельный слой логики, отделяющий бизнес-правила от инфраструктурных операций. При использовании class-validator в связке с классами DTO (Data Transfer Object) проверка структуры и корректности данных выполняется до момента их передачи в ORM или драйвер базы данных.

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

DTO-класс формирует контракт входных данных. Он определяет, какие поля допустимы и какие ограничения на них накладываются. Валидация через class-validator опирается на декораторы, описывающие правила прямо над свойствами класса.

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

class CreateUserDto {
  @IsString()
  name;

  @IsInt()
  @Min(0)
  @Max(120)
  age;
}

В этом примере задаётся базовая структура объекта пользователя. Поле name обязано быть строкой, а age — целым числом в диапазоне от 0 до 120.

Преобразование входных данных перед валидацией

Данные, поступающие извне (например, из HTTP-запроса), изначально имеют тип any или обычный объект без прототипа класса. Для корректной работы class-validator требуется преобразование в экземпляр DTO.

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

import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';

const dto = plainToInstance(CreateUserDto, requestBody);
const errors = await validate(dto);

Без этого шага декораторы не будут корректно интерпретироваться, так как отсутствует связь с классом.

Проверка перед сохранением в ORM

Наиболее распространённый сценарий — интеграция с ORM, например TypeORM. Валидация выполняется в сервисном слое до вызова save.

async function createUser(data) {
  const dto = plainToInstance(CreateUserDto, data);

  const errors = await validate(dto);
  if (errors.length > 0) {
    throw new Error('Validation failed');
  }

  return userRepository.save(dto);
}

Такой подход гарантирует, что в базу данных попадут только данные, прошедшие все правила валидации.

Жёсткая остановка через validateOrReject

Вместо ручной обработки массива ошибок используется validateOrReject, автоматически выбрасывающий исключение при нарушениях.

import { validateOrReject } from 'class-validator';

async function createUser(data) {
  const dto = plainToInstance(CreateUserDto, data);

  await validateOrReject(dto);

  return userRepository.save(dto);
}

Это упрощает контроль потока выполнения, так как сохранение не произойдёт при наличии ошибок.

Защита от лишних полей

При сохранении в базу критично ограничивать входящие данные только разрешёнными свойствами. class-validator предоставляет механизмы фильтрации через дополнительные опции валидации.

import { validate } from 'class-validator';

const errors = await validate(dto, {
  whitelist: true,
  forbidNonWhitelisted: true,
});

whitelist удаляет поля, не описанные в DTO, а forbidNonWhitelisted превращает наличие лишних полей в ошибку.

Такой механизм предотвращает массовое присваивание (mass assignment), при котором пользователь может попытаться передать поля, отсутствующие в бизнес-логике, например isAdmin, role, balance.

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

При сохранении сложных объектов, содержащих вложенные сущности, требуется рекурсивная проверка.

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

class AddressDto {
  @IsString()
  city;
}

class CreateUserDto {
  @IsString()
  name;

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

Без ValidateNested вложенный объект не проходит проверку, так как валидация не распространяется рекурсивно автоматически.

Асинхронная валидация перед записью

Некоторые ограничения требуют обращения к базе данных, например проверка уникальности email.

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

@ValidatorConstraint({ async: true })
class IsEmailUnique implements ValidatorConstraintInterface {
  async validate(email) {
    const user = await userRepository.findOne({ wh ere: { email } });
    return !user;
  }
}

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

import { Validate } from 'class-validator';

class CreateUserDto {
  @Validate(IsEmailUnique)
  email;
}

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

Связь валидации и транзакций

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

Типичный поток:

  1. Преобразование входных данных в DTO
  2. Валидация синхронных правил
  3. Проверка асинхронных ограничений
  4. Открытие транзакции
  5. Повторная проверка критичных условий при необходимости
  6. Сохранение сущностей
  7. Фиксация транзакции

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

Кастомные правила для бизнес-логики сохранения

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

@ValidatorConstraint()
class IsAdult implements ValidatorConstraintInterface {
  validate(age) {
    return age >= 18;
  }
}

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

class CreateUserDto {
  @Validate(IsAdult)
  age;
}

Такая проверка становится частью доменной модели и применяется перед сохранением так же, как и встроенные правила.

Согласование валидации с моделью базы данных

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

  • уникальные индексы
  • NOT NULL ограничения
  • CHECK constraints
  • внешние ключи

Слой class-validator предотвращает большинство ошибок заранее, снижая количество исключений на уровне БД и повышая предсказуемость операций сохранения.

Обработка ошибок валидации перед сохранением

Результат validate содержит структуру ошибок с деталями по каждому полю. Эти данные обычно трансформируются в формат, пригодный для логирования или возврата на уровень API.

const errors = await validate(dto);

const formatted = errors.map(err => ({
  field: err.property,
  constraints: err.constraints,
}));

Такой подход позволяет точно определить причину отказа сохранения и привязать её к конкретному полю объекта.

Оптимизация процесса перед записью

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

  • отключение лишних проверок через skipMissingProperties
  • группировка правил через validation groups
  • кэширование результатов асинхронных проверок
  • минимизация вложенности DTO

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

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

Валидация перед сохранением обычно размещается между контроллером и репозиторием. Такой слой обеспечивает единый входной фильтр для всех операций записи, независимо от источника данных: HTTP, очередь сообщений или внутренние вызовы.

Разделение ответственности:

  • контроллер — получение данных
  • DTO + class-validator — проверка структуры
  • сервис — бизнес-логика
  • репозиторий — сохранение в базу данных