В системах, где данные проходят через повторяющиеся проверки (например, в API-слое, очередях сообщений или при многоэтапной обработке DTO), валидация становится заметным источником нагрузки. Особенно это проявляется при использовании схем с большим количеством правил: регулярные выражения, вложенные структуры, кастомные валидаторы, асинхронные проверки.
Библиотека class-validator выполняет проверку
декларативно, но не включает встроенного механизма кеширования
результатов. Это означает, что при повторном вызове
validate() или validateSync() один и тот же
объект будет проходить полный цикл проверок заново, даже если его
состояние не изменилось.
Кеширование валидации применяется для следующих целей:
Поведение кеша в контексте class-validator ограничено
несколькими факторами:
Из-за этого кеширование требует строгого определения ключа и границ актуальности данных.
Наиболее безопасный вариант — использование WeakMap, где
ключом выступает сам объект:
const validationCache = new WeakMap();
async function validateWithCache(dto) {
if (validationCache.has(dto)) {
return validationCache.get(dto);
}
const result = await validate(dto);
validationCache.set(dto, result);
return result;
}
Преимущества:
Ограничения:
Если идентичность объекта не гарантируется, используется сериализация:
import stableStringify from "fast-json-stable-stringify";
const cache = new Map();
async function validateWithSerializedCache(dto) {
const key = stableStringify(dto);
if (cache.has(key)) {
return cache.get(key);
}
const result = await validate(dto);
cache.set(key, result);
return result;
}
Ключевое требование — детерминированная сериализация. Обычный
JSON.stringify может давать нестабильный порядок ключей,
что приводит к разным кеш-ключам для одинаковых структур.
Проблемные случаи:
undefinedДля повышения эффективности вместо полной сериализации используется хеширование:
import crypto from "crypto";
function hashDto(dto) {
return crypto
.createHash("sha256")
.update(JSON.stringify(dto))
.digest("hex");
}
Данный подход снижает стоимость хранения ключей в кеше и ускоряет поиск, но сохраняет проблему нестабильной сериализации при сложных структурах.
class-validator позволяет создавать собственные
ограничения через ValidatorConstraintInterface. Именно
внутри таких валидаторов чаще всего возникает необходимость
кеширования.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
} from "class-validator";
const externalCache = new Map();
@ValidatorConstraint({ async: true })
export class IsUniqueEmailConstraint implements ValidatorConstraintInterface {
async validate(email) {
if (externalCache.has(email)) {
return externalCache.get(email);
}
const isUnique = await checkEmailInDatabase(email);
externalCache.set(email, isUnique);
return isUnique;
}
defaultMessage() {
return "Email уже используется";
}
}
Особенности такого подхода:
class-validatorАсинхронные проверки чаще всего обращаются к внешним системам:
Без кеша такие операции становятся узким местом.
Типовая структура с кешированием:
const asyncValidationCache = new Map();
async function asyncValidate(value, validatorFn) {
const key = `${validatorFn.name}:${value}`;
if (asyncValidationCache.has(key)) {
return asyncValidationCache.get(key);
}
const result = await validatorFn(value);
asyncValidationCache.set(key, result);
return result;
}
Такой слой позволяет отделить логику кеширования от самой библиотеки валидации.
class-transformer на кешированиеПри использовании class-transformer объекты DTO часто
пересоздаются:
plainToInstance(UserDto, payload);
Это приводит к следующему эффекту:
В таких сценариях предпочтение отдается кешированию по содержимому, а не по ссылке.
Ключевая проблема кеширования валидации — устаревание данных.
Причины инвалидирования:
Подходы:
1. Версионирование DTO
const key = `${dto.version}:${stableStringify(dto)}`;
2. TTL-кеширование
const cache = new Map();
function setWithTTL(key, value, ttl) {
const expires = Date.now() + ttl;
cache.set(key, { value, expires });
}
3. Контекстное разделение
const key = `${userId}:${stableStringify(dto)}`;
В архитектуре NestJS валидация часто выполняется через
ValidationPipe. Он не содержит встроенного кеширования, но
позволяет оборачивать процесс:
@Injectable()
export class CachedValidationPipe implements PipeTransform {
private cache = new Map();
async transform(value, metadata) {
const key = JSON.stringify(value);
if (this.cache.has(key)) {
return this.cache.get(key);
}
const result = await validate(value);
this.cache.set(key, result);
if (result.length > 0) {
throw new BadRequestException(result);
}
return value;
}
}
Такой подход изменяет семантику пайплайна: валидация становится частью кешируемого слоя обработки запроса.
Кеширование в контексте class-validator требует учёта
ряда ограничений:
Особенно критично это проявляется при наличии кастомных асинхронных валидаторов, зависящих от динамического состояния системы.
В ряде случаев вместо кеширования используется снижение стоимости самой валидации:
validation groupsskipMissingPropertiesclass-transformerТакой подход уменьшает необходимость хранения промежуточных результатов, сохраняя предсказуемость поведения системы.