Webhooks

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

Одним из наиболее распространённых инструментов для описания и проверки схем данных в JavaScript является библиотека Joi. Она позволяет формализовать ожидаемую структуру объекта и гарантировать, что входящие данные соответствуют заданным правилам.


Особенности webhook-пейлоадов

Webhook представляет собой HTTP-запрос, отправляемый системой-источником при наступлении события. Обычно это POST-запрос с JSON-телом, содержащим:

  • тип события
  • идентификатор источника
  • временную метку
  • полезную нагрузку (payload)
  • дополнительные метаданные

Пример типичного JSON:

{
  "event": "payment.success",
  "timestamp": 1715330000,
  "data": {
    "orderId": "A123",
    "amount": 1500,
    "currency": "USD"
  }
}

Даже если структура кажется стабильной, на практике возможны вариации:

  • отсутствие обязательных полей
  • неожиданные типы данных (строка вместо числа)
  • дополнительные поля от стороннего сервиса
  • изменение формата без уведомления

Базовый подход к валидации через Joi

Основная идея использования 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()
    })
  })
);

Такой подход позволяет поддерживать несколько форматов в одном обработчике.


Асинхронная обработка и интеграция с middleware

В серверных приложениях 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 содержит:

  • путь к полю
  • тип ошибки
  • описание нарушения

Это упрощает диагностику проблем при интеграции с внешними сервисами.