Валидация входных данных перед сохранением в базу данных
рассматривается как отдельный слой логики, отделяющий бизнес-правила от
инфраструктурных операций. При использовании
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, например
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, автоматически выбрасывающий исключение
при нарушениях.
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;
}
Такая проверка выполняется до сохранения и предотвращает дублирование данных на уровне приложения.
В сценариях с транзакциями валидация выполняется до их открытия или внутри транзакционного контекста, если проверка зависит от промежуточного состояния базы.
Типичный поток:
Особое внимание требуется при конкурентных операциях, где уникальность данных может нарушаться между моментом валидации и моментом записи.
Стандартных декораторов недостаточно для сложных доменных ограничений. В таких случаях используется создание пользовательских валидаторов.
@ValidatorConstraint()
class IsAdult implements ValidatorConstraintInterface {
validate(age) {
return age >= 18;
}
}
Использование:
class CreateUserDto {
@Validate(IsAdult)
age;
}
Такая проверка становится частью доменной модели и применяется перед сохранением так же, как и встроенные правила.
Валидация на уровне приложения не заменяет ограничения базы данных, а дополняет их. В базе данных остаются:
Слой class-validator предотвращает большинство ошибок
заранее, снижая количество исключений на уровне БД и повышая
предсказуемость операций сохранения.
Результат validate содержит структуру ошибок с деталями
по каждому полю. Эти данные обычно трансформируются в формат, пригодный
для логирования или возврата на уровень API.
const errors = await validate(dto);
const formatted = errors.map(err => ({
field: err.property,
constraints: err.constraints,
}));
Такой подход позволяет точно определить причину отказа сохранения и привязать её к конкретному полю объекта.
При высоконагруженных системах валидация становится значимой частью времени обработки запроса. Для оптимизации применяются следующие подходы:
skipMissingPropertiesvalidation groupsЭти меры уменьшают стоимость подготовки данных перед сохранением и снижают нагрузку на сервисный слой.
Валидация перед сохранением обычно размещается между контроллером и репозиторием. Такой слой обеспечивает единый входной фильтр для всех операций записи, независимо от источника данных: HTTP, очередь сообщений или внутренние вызовы.
Разделение ответственности: