Middleware в Express и Koa выступают точкой расширения цепочки обработки запроса, где можно внедрять валидацию, нормализацию и контроль данных до попадания в бизнес-логику. При интеграции с Superstruct основная задача middleware сводится к проверке входящих данных (body, query, params, headers) и приведению их к предсказуемому формату, исключая дальнейшее распространение некорректных значений по приложению.
Superstruct предоставляет декларативный способ описания структур данных и их валидации. В контексте middleware это превращается в слой, который формализует контракт между HTTP-запросом и обработчиком.
Типовая схема middleware с использованием Superstruct включает три этапа:
validate или createКлючевая особенность заключается в том, что Superstruct не навязывает runtime-архитектуру, поэтому middleware остаётся тонким адаптером между HTTP-фреймворком и схемой.
import { object, string, number, validate } from 'superstruct';
const UserSchema = object({
name: string(),
age: number(),
});
function validateBody(struct) {
return (req, res, next) => {
const [error, data] = validate(req.body, struct);
if (error) {
return next(error);
}
req.body = data;
next();
};
}
Такой подход делает middleware универсальным: он не зависит от Express или Koa логики, а лишь адаптирует входные данные.
Express использует цепочку функций (req, res, next), что
позволяет внедрять Superstruct как промежуточный слой между парсером
тела запроса и контроллером.
import { validate } from 'superstruct';
const createBodyValidator = (struct) => {
return (req, res, next) => {
const [error, result] = validate(req.body, struct);
if (error) {
return res.status(400).json({
message: 'Validation error',
error: error.message,
});
}
req.body = result;
next();
};
};
Такой middleware выполняет две функции:
Query часто содержит строки, требующие приведения типов. Superstruct позволяет описывать строгую схему, но преобразование типов обычно выполняется отдельно.
import { object, string, number, coerce } from 'superstruct';
const QuerySchema = object({
page: coerce(number(), string(), (value) => parseInt(value, 10)),
search: string(),
});
const validateQuery = (struct) => (req, res, next) => {
const [error, data] = validate(req.query, struct);
if (error) {
return res.status(400).send(error.message);
}
req.query = data;
next();
};
Параметры маршрута обычно более стабильны, но также требуют строгой проверки, особенно при работе с идентификаторами.
const ParamsSchema = object({
id: string(),
});
app.get(
'/users/:id',
validateParams(ParamsSchema),
handler
);
При масштабировании приложения повторяющиеся функции валидации становятся проблемой. Решение — фабрики middleware.
const validateRequest = ({ body, query, params }) => {
return (req, res, next) => {
try {
if (body) {
const [bodyError, bodyData] = validate(req.body, body);
if (bodyError) throw bodyError;
req.body = bodyData;
}
if (query) {
const [queryError, queryData] = validate(req.query, query);
if (queryError) throw queryError;
req.query = queryData;
}
if (params) {
const [paramsError, paramsData] = validate(req.params, params);
if (paramsError) throw paramsError;
req.params = paramsData;
}
next();
} catch (e) {
res.status(400).json({ message: e.message });
}
};
};
Такой подход позволяет описывать контракт маршрута декларативно:
app.post(
'/users',
validateRequest({
body: UserSchema,
}),
createUserHandler
);
Koa использует ctx и async/await, что
меняет структуру middleware. Здесь Superstruct интегрируется более
естественно благодаря асинхронной модели.
const validateBody = (struct) => {
return async (ctx, next) => {
const [error, data] = validate(ctx.request.body, struct);
if (error) {
ctx.status = 400;
ctx.body = {
message: 'Validation error',
detail: error.message,
};
return;
}
ctx.request.body = data;
await next();
};
};
const validateQuery = (struct) => async (ctx, next) => {
const [error, data] = validate(ctx.query, struct);
if (error) {
ctx.throw(400, error.message);
}
ctx.query = data;
await next();
};
Koa позволяет централизованно выбрасывать ошибки через
ctx.throw, что упрощает унификацию обработки ошибок.
При использовании middleware важно стандартизировать формат ошибок. Superstruct возвращает объект ошибки, который можно преобразовать в структурированный ответ.
function formatStructError(error) {
return {
type: error.type,
path: error.path,
message: error.message,
};
}
В Express это интегрируется через error middleware:
app.use((err, req, res, next) => {
res.status(400).json({
error: formatStructError(err),
});
});
В Koa аналогично:
app.on('error', (err, ctx) => {
ctx.body = {
error: formatStructError(err),
};
});
Superstruct позволяет строить композиции схем, которые затем напрямую отражаются в middleware.
import { object, array, string } from 'superstruct';
const Address = object({
city: string(),
street: string(),
});
const User = object({
name: string(),
addresses: array(Address),
});
Middleware при этом остаётся одинаковым, независимо от глубины структуры.
При частом создании middleware важно учитывать:
Схемы Superstruct рекомендуется определять вне middleware-фабрик, чтобы избежать пересоздания объектов при каждом запросе.
const schema = object({
email: string(),
});
Superstruct часто используется как runtime-слой поверх TypeScript. Middleware в этом случае становится механизмом гарантии соответствия типов во время выполнения.
Типовой паттерн:
Superstruct позволяет добавлять кастомные валидаторы, которые особенно полезны в middleware.
import { define, string } from 'superstruct';
const Email = define('Email', (value) => {
return typeof value === 'string' && value.includes('@');
});
const schema = object({
email: Email,
});
Такие структуры повышают выразительность middleware без усложнения логики Express/Koa.
В крупных приложениях middleware на Superstruct часто становится частью слоистой архитектуры:
Middleware выполняет роль фильтра, который обеспечивает, что доменный слой работает только с корректными данными.
Не всегда требуется проверять весь объект. Superstruct позволяет описывать частичные структуры, которые удобно использовать в PATCH-эндпоинтах.
import { partial, object, string } from 'superstruct';
const UpdateUser = partial(object({
name: string(),
}));
Middleware при этом не изменяется, меняется только схема.