В системах, где сервисы обмениваются событиями через HTTP-запросы, особое значение приобретает проверка входящих данных. Webhook-запросы поступают извне, часто от сторонних сервисов, и их структура может меняться, быть неполной или содержать неожиданные значения. Именно поэтому слой валидации становится обязательной частью серверной логики.
Одним из наиболее распространённых инструментов для описания и проверки схем данных в JavaScript является библиотека Joi. Она позволяет формализовать ожидаемую структуру объекта и гарантировать, что входящие данные соответствуют заданным правилам.
Webhook представляет собой HTTP-запрос, отправляемый системой-источником при наступлении события. Обычно это POST-запрос с JSON-телом, содержащим:
Пример типичного JSON:
{
"event": "payment.success",
"timestamp": 1715330000,
"data": {
"orderId": "A123",
"amount": 1500,
"currency": "USD"
}
}
Даже если структура кажется стабильной, на практике возможны вариации:
Основная идея использования Joi заключается в описании схемы объекта, которую затем можно применить к входящим данным.
import Joi from 'joi';
const webhookSchema = Joi.object({
event: Joi.string().required(),
timestamp: Joi.number().integer().required(),
data: Joi.object({
orderId: Joi.string().required(),
amount: Joi.number().positive().required(),
currency: Joi.string().length(3).required()
}).required()
});
Такая схема задаёт строгие правила:
event обязателен и должен быть строкойtimestamp обязан быть целым числомdata должен быть объектом с фиксированной
структуройПосле определения схемы выполняется валидация:
const result = webhookSchema.validate(req.body);
if (result.error) {
return res.status(400).json({
message: 'Invalid webhook payload',
details: result.error.details
});
}
const validData = result.value;
Библиотека возвращает:
error — объект ошибки, если данные не соответствуют
схемеvalue — нормализованные данные (включая преобразования,
если они заданы)В webhook-ах часто встречаются дополнительные или необязательные параметры. Joi позволяет явно управлять их наличием.
const schema = Joi.object({
event: Joi.string().required(),
retry: Joi.boolean().default(false),
source: Joi.string().optional()
});
Здесь:
retry автоматически примет значение false,
если не переданsource может отсутствовать без ошибкиWebhook-и часто содержат сложные вложенные объекты. Joi поддерживает глубокую валидацию:
const schema = Joi.object({
event: Joi.string().required(),
data: Joi.object({
user: Joi.object({
id: Joi.string().required(),
email: Joi.string().email().required()
}),
items: Joi.array().items(
Joi.object({
sku: Joi.string().required(),
quantity: Joi.number().integer().min(1)
})
)
})
});
Такая структура позволяет контролировать каждый уровень вложенности, включая массивы объектов.
Webhook-события часто имеют фиксированный набор типов. Joi позволяет
задавать допустимые значения через valid.
const schema = Joi.object({
event: Joi.string().valid(
'payment.success',
'payment.failed',
'payment.refunded'
).required()
});
Это исключает обработку неизвестных событий на уровне валидации.
Joi способен не только проверять, но и преобразовывать данные.
const schema = Joi.object({
timestamp: Joi.number().timestamp().required(),
amount: Joi.number().precision(2).required()
});
Также можно явно приводить типы:
const schema = Joi.object({
amount: Joi.number().required(),
normalizedAmount: Joi.number().default(Joi.ref('amount'))
});
В webhook-запросах часто присутствуют дополнительные данные. Их обработка зависит от политики сервера.
const schema = Joi.object({
event: Joi.string().required(),
data: Joi.object().required()
}).unknown(false);
Опция unknown(false) запрещает любые поля, не описанные
в схеме. Это повышает предсказуемость обработки.
Иногда структура webhook может отличаться в зависимости от источника.
const schema = Joi.alternatives().try(
Joi.object({
type: Joi.string().valid('A').required(),
payload: Joi.object({
id: Joi.string().required()
})
}),
Joi.object({
type: Joi.string().valid('B').required(),
data: Joi.object({
uuid: Joi.string().required()
})
})
);
Такой подход позволяет поддерживать несколько форматов в одном обработчике.
В серверных приложениях Joi часто используется как middleware:
function validateWebhook(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details);
}
req.body = value;
next();
};
}
Это позволяет централизовать логику валидации и использовать её в маршрутах:
app.post('/webhook', validateWebhook(webhookSchema), handler);
Использование Joi в обработке webhook-запросов снижает вероятность:
При этом схема становится формальным контрактом между сервисами, описывающим допустимые входные данные.
В некоторых системах структура webhook зависит от внешних параметров:
const createSchema = (currencyList) => Joi.object({
currency: Joi.string().valid(...currencyList).required(),
amount: Joi.number().required()
});
Такой подход позволяет адаптировать валидацию под конфигурацию системы без переписывания логики.
Joi предоставляет детализированную информацию о нарушениях:
const { error } = schema.validate(data);
if (error) {
console.log(error.details);
}
Каждый элемент details содержит:
Это упрощает диагностику проблем при интеграции с внешними сервисами.