Middleware для Express/Koa

Middleware в Express и Koa выступают точкой расширения цепочки обработки запроса, где можно внедрять валидацию, нормализацию и контроль данных до попадания в бизнес-логику. При интеграции с Superstruct основная задача middleware сводится к проверке входящих данных (body, query, params, headers) и приведению их к предсказуемому формату, исключая дальнейшее распространение некорректных значений по приложению.

Superstruct предоставляет декларативный способ описания структур данных и их валидации. В контексте middleware это превращается в слой, который формализует контракт между HTTP-запросом и обработчиком.


Типовая схема middleware с использованием Superstruct включает три этапа:

  1. Извлечение данных из запроса
  2. Проверка через validate или create
  3. Передача управления дальше либо генерация ошибки

Ключевая особенность заключается в том, что 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 логики, а лишь адаптирует входные данные.


Middleware для Express: структурная валидация запросов

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

Валидация body

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-параметров

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();
};

Валидация params

Параметры маршрута обычно более стабильны, но также требуют строгой проверки, особенно при работе с идентификаторами.

const ParamsSchema = object({
  id: string(),
});

app.get(
  '/users/:id',
  validateParams(ParamsSchema),
  handler
);

Middleware фабрики для переиспользования схем

При масштабировании приложения повторяющиеся функции валидации становятся проблемой. Решение — фабрики 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
);

Middleware в Koa: работа через контекст

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();
  };
};

Работа с query и params в Koa

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, что упрощает унификацию обработки ошибок.


Централизованная обработка ошибок Superstruct

При использовании 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),
  };
});

Композиция middleware и повторное использование схем

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 в этом случае становится механизмом гарантии соответствия типов во время выполнения.

Типовой паттерн:

  • TypeScript описывает контракт на этапе компиляции
  • Superstruct подтверждает контракт на этапе выполнения
  • middleware связывает HTTP и структуру данных

Расширение 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 часто становится частью слоистой архитектуры:

  • транспортный слой (Express/Koa middleware)
  • слой валидации (Superstruct)
  • доменный слой (сервисы)
  • слой данных (репозитории)

Middleware выполняет роль фильтра, который обеспечивает, что доменный слой работает только с корректными данными.


Гибридные сценарии: частичная валидация

Не всегда требуется проверять весь объект. Superstruct позволяет описывать частичные структуры, которые удобно использовать в PATCH-эндпоинтах.

import { partial, object, string } from 'superstruct';

const UpdateUser = partial(object({
  name: string(),
}));

Middleware при этом не изменяется, меняется только схема.