Express middleware

В архитектуре приложений на Express middleware представляет собой цепочку функций, через которые проходит запрос до попадания в обработчик маршрута. Каждая функция может модифицировать req, res, завершать ответ или передавать управление дальше через next().

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

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


Базовая схема Joi для входных данных

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

import Joi fr om "joi";

const userSchema = Joi.object({
  name: Joi.string().min(2).max(30).required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(0).optional()
});

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

const result = userSchema.validate(req.body);

Результат содержит либо value, либо error.


Middleware-фабрика для валидации

В Express удобно использовать универсальный middleware, принимающий схему и источник данных.

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

    if (error) {
      return next(error);
    }

    req[property] = value;
    next();
  };
};

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

  • req.body
  • req.params
  • req.query

Использование middleware в маршрутах

app.post(
  "/users",
  validate(userSchema, "body"),
  (req, res) => {
    res.json({ user: req.body });
  }
);

Для параметров маршрута:

const idSchema = Joi.object({
  id: Joi.string().alphanum().length(24).required()
});

app.get(
  "/users/:id",
  validate(idSchema, "params"),
  (req, res) => {
    res.send(req.params.id);
  }
);

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

Query часто содержит строки, требующие приведения типов:

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

app.get(
  "/posts",
  validate(querySchema, "query"),
  (req, res) => {
    res.json(req.query);
  }
);

Joi автоматически приводит строки к числам при включённой опции convert.


Обработка ошибок Joi

Ошибка валидации имеет структуру ValidationError и содержит массив деталей:

app.use((err, req, res, next) => {
  if (err.isJoi) {
    return res.status(400).json({
      message: "Validation error",
      details: err.details.map(d => ({
        field: d.path.join("."),
        message: d.message
      }))
    });
  }

  next(err);
});

Каждый элемент details описывает конкретное нарушение схемы.


Управление поведением валидации

Joi предоставляет параметры, влияющие на стратегию проверки:

abortEarly

Останавливает проверку после первой ошибки.

schema.validate(data, { abortEarly: false });

stripUnknown

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

schema.validate(data, { stripUnknown: true });

presence

Управляет обязательностью по умолчанию:

Joi.object({
  name: Joi.string()
}).prefs({ presence: "required" });

Кастомные сообщения об ошибках

const schema = Joi.object({
  email: Joi.string()
    .email({ tlds: false })
    .messages({
      "string.email": "Некорректный email"
    })
});

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


Вложенные структуры и композиция схем

Joi поддерживает вложенные объекты и повторное использование схем.

const addressSchema = Joi.object({
  city: Joi.string().required(),
  street: Joi.string().required()
});

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

Композиция упрощает поддержку сложных моделей данных.


Валидация массивов

const schema = Joi.object({
  tags: Joi.array().items(Joi.string().min(2)).min(1)
});

Можно ограничивать уникальность:

Joi.array().items(Joi.string()).unique()

Приведение типов и нормализация данных

Joi способен нормализовать входящие данные:

const schema = Joi.object({
  active: Joi.boolean(),
  count: Joi.number()
});

Запрос:

{
  "active": "true",
  "count": "10"
}

будет преобразован в:

{
  "active": true,
  "count": 10
}

Разделение ответственности middleware

В типичной архитектуре Express middleware с Joi выполняет только одну задачу — проверку структуры данных. После успешной валидации управление передаётся контроллеру без дополнительных проверок типов.

const createUserController = (req, res) => {
  const data = req.body;
  res.json({ created: true, user: data });
};

Централизованная стратегия валидации

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

/schemas
  user.schema.js
  auth.schema.js
/middleware
  validate.js
/routes

Такой подход позволяет поддерживать единообразие правил валидации и снижает дублирование логики.


Валидация сложных запросов

Сложные API-запросы часто включают одновременно body, params и query. Для таких случаев создаётся комбинированный middleware:

const validateRequest = (schemas) => {
  return (req, res, next) => {
    const sources = ["body", "params", "query"];

    for (const key of sources) {
      if (schemas[key]) {
        const { error, value } = schemas[key].validate(req[key], {
          abortEarly: false,
          stripUnknown: true
        });

        if (error) return next(error);
        req[key] = value;
      }
    }

    next();
  };
};

Роль валидации в безопасности API

Валидация на уровне middleware снижает риск:

  • некорректных типов данных в бизнес-логике
  • SQL/NoSQL-инъекций через непроверенные поля
  • неожиданных структур JSON
  • перегрузки API лишними полями

Joi не является системой безопасности, но выступает важным слоем фильтрации входящих данных до попадания в ядро приложения.


Поведение при расширении схем

Схемы могут быть расширены без изменения middleware:

const baseSchema = Joi.object({
  name: Joi.string().required()
});

const extendedSchema = baseSchema.keys({
  role: Joi.string().valid("user", "admin")
});

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