В архитектуре приложений на Express middleware представляет собой
цепочку функций, через которые проходит запрос до попадания в обработчик
маршрута. Каждая функция может модифицировать req,
res, завершать ответ или передавать управление дальше через
next().
Валидация входящих данных в таких цепочках критична, поскольку контроллеры не должны работать с некорректными или неполными структурами. Для этой задачи часто используется Joi — декларативная библиотека описания схем данных.
Joi позволяет описывать структуру входных данных как набор правил: типы, ограничения, обязательность полей, вложенные объекты, массивы и кастомные проверки. В связке с Express middleware она становится слоем защиты между HTTP-запросом и бизнес-логикой.
Схемы 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.
В 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.bodyreq.paramsreq.queryapp.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 часто содержит строки, требующие приведения типов:
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.
Ошибка валидации имеет структуру 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 предоставляет параметры, влияющие на стратегию проверки:
Останавливает проверку после первой ошибки.
schema.validate(data, { abortEarly: false });
Удаляет поля, не описанные в схеме.
schema.validate(data, { stripUnknown: true });
Управляет обязательностью по умолчанию:
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
}
В типичной архитектуре 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();
};
};
Валидация на уровне middleware снижает риск:
Joi не является системой безопасности, но выступает важным слоем фильтрации входящих данных до попадания в ядро приложения.
Схемы могут быть расширены без изменения middleware:
const baseSchema = Joi.object({
name: Joi.string().required()
});
const extendedSchema = baseSchema.keys({
role: Joi.string().valid("user", "admin")
});
Такой механизм полезен при наследовании моделей данных в разных контекстах API.