Валидация с внешними API

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

Ключевая особенность асинхронной модели заключается в том, что валидаторы могут возвращать Promise<boolean>, а весь процесс валидации становится неблокирующим и управляемым через await на уровне validate().


Базовый принцип интеграции внешнего API

Внешний API используется внутри кастомного валидатора. Логика выносится в класс, помеченный декоратором @ValidatorConstraint, а сама проверка выполняется в методе validate.

Типовая структура:

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

@ValidatorConstraint({ async: true })
class IsEmailAvailableConstraint {
  async validate(email, args) {
    const response = await fetch(`https://api.example.com/users/check-email?email=${email}`);
    const data = await response.json();

    return data.available === true;
  }

  defaultMessage(args) {
    return 'Email уже используется';
  }
}

Декларация асинхронности через { async: true } является обязательной при использовании внешних запросов. Без этого результата Promise может интерпретироваться некорректно.


Подключение кастомного валидатора к DTO

После определения constraint создаётся декоратор:

import { Validate } from 'class-validator';

export function IsEmailAvailable(validationOptions) {
  return function (object, propertyName) {
    Validate(IsEmailAvailableConstraint, validationOptions)(
      object,
      propertyName
    );
  };
}

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

class CreateUserDto {
  @IsEmailAvailable()
  email;
}

Работа с HTTP-клиентами внутри валидаторов

Вместо fetch часто применяется axios или специализированные HTTP-клиенты, особенно при необходимости обработки таймаутов, повторов и interceptors.

import axios from 'axios';

@ValidatorConstraint({ async: true })
class IsUsernameUniqueConstraint {
  async validate(username) {
    const { data } = await axios.get(
      `https://api.example.com/users/exists`,
      {
        params: { username },
        timeout: 2000
      }
    );

    return data.exists === false;
  }
}

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


Обработка таймаутов и отказоустойчивость

Валидация через внешние API создаёт зависимость от сетевой доступности. При проектировании логики учитываются сценарии деградации:

  • таймаут запроса
  • недоступность сервиса
  • частичные ошибки ответа
async validate(value) {
  try {
    const response = await axios.get('https://api.example.com/check', {
      timeout: 1500
    });

    return response.data.valid === true;
  } catch (error) {
    return false;
  }
}

Возврат false в случае ошибки API часто используется как стратегия fail-safe, однако в некоторых системах применяется fail-open логика, где внешняя ошибка не блокирует основную операцию.


Кэширование результатов внешней валидации

Для уменьшения нагрузки на API применяется кэширование. Валидация часто выполняется многократно для одинаковых значений, поэтому мемоизация существенно снижает латентность.

const cache = new Map();

@ValidatorConstraint({ async: true })
class IsProductValidConstraint {
  async validate(productId) {
    if (cache.has(productId)) {
      return cache.get(productId);
    }

    const { data } = await axios.get(
      `https://api.example.com/products/${productId}`
    );

    const isValid = data.active === true;
    cache.set(productId, isValid);

    return isValid;
  }
}

При промышленной эксплуатации вместо Map используется Redis или аналогичное распределённое хранилище.


Параллельные запросы и агрегация данных

Некоторые сценарии требуют проверки нескольких внешних источников одновременно. В этом случае используется Promise.all, что снижает общее время ожидания.

async validate(value) {
  const [serviceA, serviceB] = await Promise.all([
    axios.get(`https://api-a.com/check/${value}`),
    axios.get(`https://api-b.com/check/${value}`)
  ]);

  return serviceA.data.ok && serviceB.data.ok;
}

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


Ограничение частоты запросов (rate limiting)

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

  • throttling
  • debounce
  • очереди запросов

Пример простого throttle-слоя:

const lastCall = new Map();

function canCall(key, delay = 1000) {
  const now = Date.now();
  const prev = lastCall.get(key) || 0;

  if (now - prev < delay) return false;

  lastCall.set(key, now);
  return true;
}

Использование внутри валидатора:

async validate(value) {
  if (!canCall(value)) return true;

  const { data } = await axios.get(`/api/check/${value}`);
  return data.ok;
}

Интеграция с бизнес-реестрами и сторонними сервисами

Внешние API часто представляют собой:

  • реестры пользователей
  • сервисы оплаты
  • системы KYC/AML
  • каталоги товаров
  • сервисы геолокации

Каждый тип данных требует отдельной стратегии интерпретации ответа.

Пример проверки существования пользователя в реестре:

@ValidatorConstraint({ async: true })
class UserExistsConstraint {
  async validate(userId) {
    const { data } = await axios.get(
      `https://registry.example.com/users/${userId}`
    );

    return data.found === true;
  }
}

Композиция нескольких внешних проверок

В сложных системах одна сущность может валидироваться по нескольким независимым правилам:

class OrderDto {
  @IsProductValid()
  productId;

  @IsUserActive()
  userId;

  @IsShippingAvailable()
  addressId;
}

Каждый декоратор может обращаться к отдельному API, формируя распределённую систему валидации.


Управление порядком выполнения асинхронных валидаторов

class-validator выполняет проверки параллельно, однако бизнес-логика иногда требует последовательности. Для этого используется объединённый валидатор:

@ValidatorConstraint({ async: true })
class ComplexConstraint {
  async validate(value) {
    const step1 = await axios.get(`/api/step1/${value}`);
    if (!step1.data.ok) return false;

    const step2 = await axios.get(`/api/step2/${value}`);
    return step2.data.allowed === true;
  }
}

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


Обработка форматов ответов внешних API

Различные сервисы возвращают разные структуры данных, что требует нормализации:

function normalizeResponse(response) {
  return {
    valid: response.data?.result?.status === 'ok'
  };
}

async validate(value) {
  const response = await axios.get(`/api/check/${value}`);
  const normalized = normalizeResponse(response);

  return normalized.valid;
}

Выделение нормализации в отдельную функцию снижает связанность валидатора с конкретным API.


Ошибки, возникающие при внешней валидации

На практике выделяются несколько типов проблем:

  • сетевые сбои
  • некорректный JSON
  • изменение API без обратной совместимости
  • задержки выше допустимых SLA

Обработка обычно унифицируется:

async validate(value) {
  try {
    const { data } = await axios.get(`/api/check/${value}`);
    return Boolean(data?.valid);
  } catch (e) {
    return false;
  }
}

Оптимизация цепочек валидации с API

При большом количестве внешних проверок применяется стратегия раннего выхода:

async validate(value) {
  if (!(await checkLocalRules(value))) return false;
  if (!(await checkCache(value))) return false;
  if (!(await checkExternalApi(value))) return false;

  return true;
}

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


Сценарии использования в распределённых архитектурах

Асинхронная валидация через внешние API особенно важна в системах:

  • микросервисной архитектуры
  • серверless-приложениях
  • BFF (Backend For Frontend) слоях
  • API gateway уровнях

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