Работа с legacy-кодом

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

Legacy-код обычно характеризуется следующими особенностями:

  • использование «плоских» объектов без классов
  • отсутствие единых моделей данных
  • смешивание бизнес-логики и валидации
  • частое использование any и динамических структур
  • поступление данных из внешних источников без промежуточных слоёв

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


Базовая проблема: class-validator работает с классами

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

Пример типичной DTO-модели:

import { IsString, IsInt, Min } from "class-validator";

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

  @IsInt()
  @Min(0)
  age: number;
}

Однако legacy-код часто оперирует такими структурами:

const user = {
  name: "Alex",
  age: "25"
};

Валидация таких объектов напрямую невозможна без приведения к классу.


Адаптация legacy-объектов через DTO-обёртки

Наиболее устойчивый подход — создание DTO-слоя, который не влияет на существующую архитектуру, а лишь оборачивает входные данные.

import { validate } from "class-validator";

async function validateUser(input) {
  const dto = Object.assign(new UserDto(), input);
  return await validate(dto);
}

В legacy-системах этот слой обычно встраивается:

  • на уровне контроллера
  • в сервисе перед бизнес-логикой
  • в адаптере внешнего API

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


Проблема типов и неявных преобразований

Legacy-код часто передаёт данные в «грязном» виде: строки вместо чисел, отсутствующие поля, вложенные структуры без формализации.

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

const normalizeUser = (input) => ({
  name: input.name,
  age: Number(input.age)
});

После нормализации выполняется валидация:

const dto = Object.assign(new UserDto(), normalizeUser(input));
const errors = await validate(dto);

В крупных системах нормализация становится отдельным этапом pipeline.


Частичная валидация в условиях неполных моделей

Legacy-данные часто приходят неполными. В таких случаях важно управлять поведением валидатора.

Используются параметры:

validate(dto, {
  skipMissingProperties: true
});

Это позволяет:

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

Пример DTO с постепенной строгостью:

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

class PatchUserDto {
  @IsOptional()
  @IsString()
  name?: string;
}

Интеграция через validateSync в legacy-сервисах

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

import { validateSync } from "class-validator";

function validateLegacy(input) {
  const dto = Object.assign(new UserDto(), input);
  return validateSync(dto);
}

Такой подход сохраняет совместимость с:

  • синхронными API
  • middleware старых фреймворков
  • монолитными сервисами без async pipeline

Использование групп для поэтапной миграции

Legacy-системы редко позволяют сразу заменить всю модель данных. Группы валидации позволяют вводить правила постепенно.

import { IsString } from "class-validator";

class UserDto {
  @IsString({ groups: ["v2"] })
  name: string;
}

Вызов:

validate(dto, { groups: ["v2"] });

Это даёт возможность:

  • поддерживать старую и новую логику параллельно
  • переключать правила через конфигурацию
  • мигрировать API без breaking changes

Обработка «грязных» объектов из внешних источников

Legacy-код часто получает данные из:

  • старых REST API
  • SOAP-сервисов
  • файловых импортов
  • очередей сообщений без схемы

Такие данные требуют дополнительной защиты через whitelist-подход.

validate(dto, {
  whitelist: true,
  forbidNonWhitelisted: true
});

Поведение:

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

Инкапсуляция legacy-слоя через адаптеры

При невозможности изменения всего приложения вводится адаптерный слой.

class UserLegacyAdapter {
  static toDto(raw) {
    const dto = new UserDto();
    dto.name = raw.user_name;
    dto.age = Number(raw.user_age);
    return dto;
  }

  static async validate(raw) {
    const dto = this.toDto(raw);
    return validate(dto);
  }
}

Такой слой решает сразу несколько задач:

  • изоляция legacy-структур
  • единая точка трансформации
  • упрощение тестирования

Декораторы в условиях legacy-бандлинга

В старых проектах часто отсутствует корректная поддержка:

  • emitDecoratorMetadata
  • reflect-metadata
  • Babel-конфигураций для декораторов

В таких случаях возможны ограничения:

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

Типичный обходной путь:

import "reflect-metadata";

и строгая проверка конфигурации сборщика.


Смешивание class-validator с неструктурированным кодом

В legacy-системах часто невозможно обеспечить чистую архитектуру. Тогда class-validator применяется точечно:

  • на входе контроллеров
  • перед записью в базу
  • при обработке сообщений очередей

Пример интеграции в сервис:

async function createUser(rawInput) {
  const dto = Object.assign(new UserDto(), rawInput);

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

  return userRepository.save(dto);
}

Ограничения применения в legacy-контексте

При работе в старых системах проявляются следующие ограничения:

  • необходимость ручного маппинга данных
  • невозможность полной автоматизации без class-transformer
  • увеличение количества DTO-классов
  • дублирование логики валидации и нормализации
  • зависимость от метаданных TypeScript

Эти ограничения не являются недостатками библиотеки, а отражают несовместимость между динамической архитектурой legacy-кода и статической моделью валидации.


Постепенная миграция архитектуры

На практике внедрение class-validator в legacy-систему происходит не как замена, а как наращивание слоя строгости.

Типовой путь миграции:

  • ввод DTO только для новых модулей
  • добавление адаптеров для старых входов
  • включение частичной валидации
  • переход на строгие схемы через forbidNonWhitelisted
  • постепенное удаление «сырых» объектов из бизнес-логики

Такая стратегия позволяет сохранить работоспособность системы без резких изменений структуры кода