Внедрение строгой валидации в уже работающую кодовую базу редко происходит одномоментно. В реальных проектах обычно присутствует смесь старого кода, частично типизированных данных, неявных контрактов между слоями и отсутствия централизованной проверки входящих данных. Библиотека Class-validator позволяет вводить валидацию постепенно, не ломая существующую архитектуру и не требуя полной переработки системы.
Ключевая идея постепенного внедрения заключается в том, чтобы валидация сначала появлялась только на границах системы, затем распространялась внутрь доменных объектов и лишь после этого становилась обязательной частью всех входных потоков данных.
Первым шагом становится добавление проверки входных данных только в точках входа: HTTP-запросы, события очередей, внешние API.
На этом этапе не вводятся DTO-классы и декораторы. Валидация выполняется вручную, чтобы зафиксировать текущие реальные данные, поступающие в систему.
function validateCreateUser(payload) {
const errors = [];
if (typeof payload.email !== 'string') {
errors.push('email must be string');
}
if (!payload.email?.includes('@')) {
errors.push('email format is invalid');
}
if (typeof payload.age !== 'number') {
errors.push('age must be number');
}
return {
valid: errors.length === 0,
errors,
};
}
Этот этап фиксирует фактическое поведение API и помогает выявить несоответствия между ожиданиями и реальностью. Он важен как точка отсчёта.
После стабилизации входных данных вводятся DTO-классы и базовые декораторы Class-validator. На этом этапе библиотека используется только для новых модулей или новых эндпоинтов.
import { IsEmail, IsInt, Min } from 'class-validator';
class CreateUserDto {
@IsEmail()
email;
@IsInt()
@Min(0)
age;
}
Валидация выполняется явно:
import { validate } from 'class-validator';
async function validateDto(dto) {
const errors = await validate(dto);
return errors;
}
Важно, что старые участки системы продолжают работать без изменений. Class-validator внедряется как дополнительный слой, а не как замена существующей логики.
При постепенной миграции неизбежно сосуществование двух подходов: ручной валидации и декларативной через декораторы.
Для этого вводится единый адаптер:
async function validateCreateUserInput(payload) {
if (payload.__legacy === true) {
return validateLegacy(payload);
}
const dto = Object.assign(new CreateUserDto(), payload);
return validate(dto);
}
Такой подход позволяет мигрировать эндпоинты по одному, не затрагивая остальную систему.
На следующем этапе добавляется преобразование plain-object в классы. Это снижает количество ручного кода и делает валидацию более предсказуемой.
import { plainToInstance } from 'class-transformer';
const dto = plainToInstance(CreateUserDto, payload);
const errors = await validate(dto);
Это особенно важно, когда входящие данные приходят из JSON и не имеют прототипов.
Class-validator предоставляет возможность усиливать строгость постепенно.
Эти настройки позволяют контролировать лишние поля:
import { validate } from 'class-validator';
await validate(dto, {
whitelist: true,
forbidNonWhitelisted: true,
});
Постепенное внедрение начинается с whitelist: true,
который просто удаляет лишние поля, не ломая поведение API. После
стабилизации можно включить forbidNonWhitelisted, который
начинает возвращать ошибки.
В больших проектах DTO обычно вводятся поэтапно.
class UpdateUserDto {}
На этом этапе система уже использует классы, но не применяет валидацию.
import { IsOptional, IsString } from 'class-validator';
class UpdateUserDto {
@IsOptional()
@IsString()
name;
}
Здесь вводится только часть правил, не затрагивая остальные поля.
import { IsString, IsEmail, IsOptional } from 'class-validator';
class UpdateUserDto {
@IsOptional()
@IsString()
name;
@IsOptional()
@IsEmail()
email;
}
Одной из сложностей постепенного внедрения является различие между POST и PATCH. PATCH-запросы требуют частичной валидации.
Class-validator решает это через @IsOptional() и
skipMissingProperties.
await validate(dto, {
skipMissingProperties: true,
});
Это позволяет валидировать только переданные поля, не требуя полного объекта.
Validation groups позволяют разделить строгие и мягкие правила в рамках одного DTO.
import { IsEmail, Length } from 'class-validator';
class UserDto {
@IsEmail({ groups: ['create'] })
email;
@Length(3, 20, { groups: ['create', 'update'] })
name;
}
Вызов:
await validate(dto, { groups: ['create'] });
Такой механизм позволяет постепенно усиливать требования для разных сценариев без дублирования классов.
При постепенном внедрении важно не переписывать контроллеры.
app.post('/users', async (req, res) => {
const dto = plainToInstance(CreateUserDto, req.body);
const errors = await validate(dto);
if (errors.length > 0) {
return res.status(400).json(errors);
}
// существующая бизнес-логика
});
Старые обработчики могут использовать ту же схему выборочно, без обязательной миграции всего API.
На уровне сервисов валидация внедряется осторожно, чтобы не нарушить внутренние зависимости.
Сначала проверяются только внешние данные:
class UserService {
async createUser(dto) {
// предполагается, что dto уже проверен
return this.repository.save(dto);
}
}
Позднее можно добавить защитную валидацию:
import { validateOrReject } from 'class-validator';
class UserService {
async createUser(dto) {
await validateOrReject(dto);
return this.repository.save(dto);
}
}
Такой переход позволяет гарантировать корректность данных на уровне домена.
При крупной кодовой базе внедрение Class-validator обычно выполняется по слоям:
forbidNonWhitelisted,
groups)Основная сложность заключается в несогласованности данных между слоями. Чтобы минимизировать риски, вводится промежуточный слой нормализации:
function normalizeUserPayload(payload) {
return {
email: String(payload.email || ''),
age: Number(payload.age || 0),
};
}
Этот слой временно стабилизирует данные до полного перехода на DTO.
В крупных системах внедрение часто привязывается к флагам:
if (featureFlags.useClassValidator) {
const dto = plainToInstance(CreateUserDto, req.body);
const errors = await validate(dto);
}
Это позволяет включать валидацию по частям: по пользователям, по сервисам или по версиям API.
Постепенное внедрение требует сохранения старого поведения. Поэтому часто вводятся параллельные контракты:
// v1 - legacy
app.post('/v1/user', legacyHandler);
// v2 - class-validator
app.post('/v2/user', modernHandler);
Такой подход обеспечивает плавный переход без нарушения существующих клиентов.
Переход к Class-validator в зрелой кодовой базе обычно развивается от: