Валидация в контексте 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)Каждый источник требует отдельной схемы или составной модели.
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()
});
Такая схема предотвращает попадание некорректных идентификаторов в слой доступа к данным.
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': 'Минимальная длина строки не соблюдена'
});
Такой подход используется для:
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() для уникальности элементовТиповой 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);
Такой подход особенно важен в микросервисной архитектуре, где контракт между сервисами должен быть строго формализован.
Использование Joi валидации на уровне запросов и ответов позволяет формировать строгий контракт:
Дополнительный уровень строгости достигается через:
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);
Такой подход применяется при:
В крупных приложениях схемы выносятся в отдельные модули:
Композиция позволяет переиспользовать части схем:
const idSchema = Joi.number().integer().required();
const userSchema = Joi.object({
id: idSchema,
name: Joi.string().required()
});
Это снижает дублирование и упрощает сопровождение.
При использовании Joi важно учитывать:
Дополнительное внимание требуется к:
allow() — расширение допустимых значенийvalid() — строгий список допустимых значенийstrip() — удаление полей из результатаСхемная валидация формирует слой, который находится между транспортным уровнем и бизнес-логикой. Такой слой выполняет:
Валидация запросов и ответов становится частью архитектурной дисциплины, а не вспомогательной функцией, что особенно критично в распределённых системах и публичных API.