Валидация запросов и ответов

Валидация в контексте HTTP-приложений выполняет роль формального контроля структуры и содержания данных, поступающих в систему и покидающих её. Библиотека Joi реализует декларативный подход, при котором данные описываются схемой, а затем сравниваются с ней во время выполнения.

Схема в Joi представляет собой объект, описывающий ожидаемую структуру данных: типы полей, ограничения, правила преобразования и условия. Такой подход позволяет отделить бизнес-логику от проверки корректности входных и выходных данных, формируя единый контракт между слоями приложения.

Базовые типы и построение схем

Основой работы выступают типизированные конструкторы:

  • Joi.string() — строковые значения
  • Joi.number() — числовые данные
  • Joi.boolean() — логические значения
  • Joi.object() — структурированные объекты
  • Joi.array() — массивы элементов

Каждый тип поддерживает цепочку методов, уточняющих ограничения:

import Joi fr om 'joi';

const schema = Joi.object({
  username: Joi.string().min(3).max(30).required(),
  age: Joi.number().integer().min(0).max(120),
  email: Joi.string().email().required()
});

Схема описывает строгую структуру объекта: обязательные и необязательные поля, диапазоны значений и форматы.

Валидация входящих запросов

В серверных приложениях на Node.js входящие данные обычно поступают из нескольких источников:

  • тело запроса (body)
  • параметры маршрута (params)
  • строка запроса (query)

Каждый источник требует отдельной схемы или составной модели.

Валидация body

const bodySchema = Joi.object({
  title: Joi.string().min(5).required(),
  content: Joi.string().min(20).required()
});

Применение в обработчике:

const { error, value } = bodySchema.validate(req.body, {
  abortEarly: false,
  stripUnknown: true
});

Ключевые опции:

  • abortEarly: false — сбор всех ошибок вместо остановки на первой
  • stripUnknown: true — удаление лишних полей

Валидация параметров маршрута

const paramsSchema = Joi.object({
  id: Joi.number().integer().required()
});

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

Валидация query-параметров

const querySchema = Joi.object({
  page: Joi.number().min(1).default(1),
  lim it: Joi.number().min(1).max(100).default(20)
});

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

Ошибки валидации и их обработка

Результат проверки Joi содержит два ключевых элемента:

  • value — нормализованные данные
  • error — объект ошибки при несоответствии схем

Структура ошибки включает массив деталей:

if (error) {
  error.details.forEach(detail => {
    console.log(detail.message);
  });
}

Каждый элемент details содержит:

  • путь к полю
  • сообщение
  • тип ошибки

Формирование единого ответа об ошибках обычно требует преобразования структуры Joi в формат API.

Кастомные сообщения и локализация

Joi поддерживает переопределение сообщений:

const schema = Joi.string().min(5).messages({
  'string.min': 'Минимальная длина строки не соблюдена'
});

Такой подход используется для:

  • унификации формата ошибок
  • локализации сообщений
  • скрытия внутренних деталей схемы

Преобразование данных (casting и sanitization)

Joi не только проверяет данные, но и приводит их к нужному виду.

Примеры преобразований:

Joi.string().trim().lowercase()
  • trim() — удаление пробелов
  • lowercase() — приведение к нижнему регистру

Числовые преобразования:

Joi.number().integer().positive()

В сочетании с convert: true (по умолчанию) входные строки могут автоматически преобразовываться в числа.

Условная валидация

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

const schema = Joi.object({
  role: Joi.string().valid('user', 'admin'),
  adminCode: Joi.string().when('role', {
    is: 'admin',
    then: Joi.required(),
    otherwise: Joi.forbidden()
  })
});

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

Работа с массивами и вложенными структурами

Сложные API часто используют вложенные объекты:

const schema = Joi.object({
  users: Joi.array().items(
    Joi.object({
      id: Joi.number().required(),
      name: Joi.string().required()
    })
  )
});

Поддерживаются ограничения:

  • min, max для длины массива
  • unique() для уникальности элементов
  • вложенные схемы любой глубины

Интеграция с Express

Типовой middleware для проверки запроса:

const validate = (schema) => (req, res, next) => {
  const { error, value } = schema.validate(req.body, {
    abortEarly: false,
    stripUnknown: true
  });

  if (error) {
    return res.status(400).json({
      errors: error.details.map(d => d.message)
    });
  }

  req.body = value;
  next();
};

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

Валидация ответа сервера

Валидация ответов используется реже, но играет важную роль в поддержании контракта API.

Основные сценарии:

  • контроль структуры выходных данных
  • предотвращение утечки лишних полей
  • стандартизация ответов
const responseSchema = Joi.object({
  id: Joi.number().required(),
  title: Joi.string().required(),
  createdAt: Joi.date().iso().required()
});

Проверка перед отправкой:

const { error, value } = responseSchema.validate(result);

if (error) {
  throw new Error('Response validation failed');
}

res.json(value);

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

Стратегии строгой типизации API

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

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

Дополнительный уровень строгости достигается через:

  • required() для обязательных полей
  • forbidden() для запрещённых полей
  • unknown(false) для запрета лишних ключей
Joi.object({
  id: Joi.number().required(),
  name: Joi.string().required()
}).unknown(false);

Расширенные возможности композиции схем

Joi поддерживает композицию через:

  • concat() — объединение схем
  • alternatives() — выбор одного из вариантов
  • when() — условные ветвления

Пример альтернатив:

const schema = Joi.alternatives().try(
  Joi.string().email(),
  Joi.number().integer()
);

Это полезно при работе с API, принимающими несколько форматов одного поля.

Асинхронная валидация

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

await schema.validateAsync(data);

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

  • проверке уникальности в базе данных
  • обращении к внешним сервисам
  • сложных вычисляемых правилах

Структурирование больших схем

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

  • schemas/user.js
  • schemas/order.js
  • schemas/common.js

Композиция позволяет переиспользовать части схем:

const idSchema = Joi.number().integer().required();

const userSchema = Joi.object({
  id: idSchema,
  name: Joi.string().required()
});

Это снижает дублирование и упрощает сопровождение.

Ограничения и особенности поведения

При использовании Joi важно учитывать:

  • преобразование типов может происходить автоматически
  • порядок вызова методов влияет на итоговое поведение
  • ошибки формируются на уровне схемы, а не бизнес-логики

Дополнительное внимание требуется к:

  • allow() — расширение допустимых значений
  • valid() — строгий список допустимых значений
  • strip() — удаление полей из результата

Контракт данных как часть архитектуры

Схемная валидация формирует слой, который находится между транспортным уровнем и бизнес-логикой. Такой слой выполняет:

  • фильтрацию некорректных данных
  • нормализацию входных значений
  • контроль структуры API
  • защиту от неожиданных входных состояний

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