Express и middleware

Валидация входящих данных в веб-приложениях на Node.js — один из ключевых слоёв защиты и стабильности. В экосистеме Express это почти всегда реализуется через middleware, позволяя отделить бизнес-логику от проверки корректности входных данных. Библиотека Ajv (Another JSON Schema Validator) становится центральным инструментом, когда требуется строгая и высокопроизводительная проверка JSON-структур по схемам.

Express не навязывает архитектуру, поэтому middleware-валидация формирует стандартизированный слой обработки запросов:

  • запрос приходит в приложение;
  • middleware проверяет структуру данных;
  • при успехе управление передаётся дальше;
  • при ошибке формируется единый ответ об ошибке.

Такой подход позволяет централизовать контроль качества входных данных.


Базовая интеграция Ajv в Express

В основе интеграции лежит создание экземпляра валидатора и его повторное использование для всех схем.

import express fr om "express";
import Ajv from "ajv";

const app = express();
app.use(express.json());

const ajv = new Ajv({
  allErrors: true,
  removeAdditional: true,
  strict: false
});

Ключевые параметры конфигурации:

  • allErrors: true — собирает все ошибки, а не останавливается на первой;
  • removeAdditional: true — удаляет лишние поля из входящего объекта;
  • strict: false — снижает жёсткость режима проверки схем.

Middleware-обёртка для схем

Основной паттерн использования Ajv в Express — фабрика middleware, принимающая JSON Schema.

const validate = (schema) => {
  const validator = ajv.compile(schema);

  return (req, res, next) => {
    const valid = validator(req.body);

    if (valid) {
      return next();
    }

    return res.status(400).json({
      message: "Validation error",
      errors: validator.errors
    });
  };
};

Здесь происходит ключевое:

  • схема компилируется один раз;
  • на каждый запрос используется уже готовая функция проверки;
  • ошибки стандартизируются в единый формат ответа.

Валидация тела запроса (request body)

Наиболее распространённый сценарий — проверка req.body.

const userSchema = {
  type: "object",
  properties: {
    email: { type: "string", format: "email" },
    password: { type: "string", minLength: 6 }
  },
  required: ["email", "password"],
  additionalProperties: false
};

app.post("/register", validate(userSchema), (req, res) => {
  res.json({ status: "ok" });
});

Особенности:

  • required определяет обязательные поля;
  • additionalProperties: false предотвращает “мусорные” данные;
  • format: "email" использует встроенные форматы Ajv.

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

Для req.query часто требуется отдельная схема, так как типы приходят в виде строк.

const querySchema = {
  type: "object",
  properties: {
    page: { type: "string", pattern: "^[0-9]+$" },
    lim it: { type: "string", pattern: "^[0-9]+$" }
  },
  required: [],
  additionalProperties: false
};

app.get("/users", validateQuery(querySchema), (req, res) => {
  res.json([]);
});

Middleware для query:

const validateQuery = (schema) => {
  const validator = ajv.compile(schema);

  return (req, res, next) => {
    const valid = validator(req.query);

    if (!valid) {
      return res.status(400).json({ errors: validator.errors });
    }

    next();
  };
};

Валидация параметров маршрута

Параметры req.params часто используются для идентификаторов ресурсов.

const paramsSchema = {
  type: "object",
  properties: {
    id: { type: "string", pattern: "^[0-9]+$" }
  },
  required: ["id"],
  additionalProperties: false
};

app.get("/users/:id", validateParams(paramsSchema), (req, res) => {
  res.json({ id: req.params.id });
});

Унификация middleware для разных источников данных

Практика показывает, что лучше использовать универсальный middleware с параметром источника данных.

const validateRequest = (schema, source = "body") => {
  const validator = ajv.compile(schema);

  return (req, res, next) => {
    const data = req[source];
    const valid = validator(data);

    if (!valid) {
      return res.status(400).json({
        source,
        errors: validator.errors
      });
    }

    next();
  };
};

Использование:

app.post("/login", validateRequest(loginSchema, "body"), handler);
app.get("/search", validateRequest(searchSchema, "query"), handler);
app.get("/item/:id", validateRequest(idSchema, "params"), handler);

Обработка ошибок в едином формате

В крупных приложениях важно не дублировать формат ошибок. Вместо прямого возврата из middleware можно пробрасывать ошибки в централизованный обработчик.

class ValidationError extends Error {
  constructor(errors) {
    super("Validation error");
    this.errors = errors;
  }
}

Middleware:

if (!valid) {
  return next(new ValidationError(validator.errors));
}

Глобальный обработчик:

app.use((err, req, res, next) => {
  if (err instanceof ValidationError) {
    return res.status(400).json({
      message: err.message,
      details: err.errors
    });
  }

  res.status(500).json({ message: "Internal error" });
});

Производительность и кеширование компиляции схем

Ajv работает быстро, но компиляция схем — дорогая операция. Поэтому критически важно:

  • компилировать схемы один раз;
  • избегать создания валидаторов внутри обработчиков запросов;
  • переиспользовать функции валидации.

Оптимальный подход:

const validators = {
  user: ajv.compile(userSchema),
  login: ajv.compile(loginSchema)
};

Динамические схемы и фабрики

Иногда схема зависит от входных параметров (например, роли пользователя).

const createSchemaByRole = (role) => {
  if (role === "admin") {
    return {
      type: "object",
      properties: {
        accessLevel: { type: "number" }
      },
      required: ["accessLevel"]
    };
  }

  return {
    type: "object",
    properties: {
      username: { type: "string" }
    },
    required: ["username"]
  };
};

Middleware:

const dynamicValidate = (roleExtractor) => {
  return (req, res, next) => {
    const schema = createSchemaByRole(roleExtractor(req));
    const validate = ajv.compile(schema);

    if (!validate(req.body)) {
      return res.status(400).json(validate.errors);
    }

    next();
  };
};

Расширение Ajv форматами и кастомными правилами

Ajv поддерживает расширение логики через кастомные keyword.

ajv.addKeyword({
  keyword: "isOdd",
  validate: (schema, data) => {
    return typeof data === "number" && data % 2 === 1;
  }
});

Схема:

const schema = {
  type: "object",
  properties: {
    value: { type: "number", isOdd: true }
  }
};

Использование middleware в сложных цепочках

Express позволяет комбинировать middleware, создавая слоистую архитектуру:

app.post(
  "/orders",
  authMiddleware,
  validateOrderSchema,
  businessLogicMiddleware,
  controller
);

Преимущества:

  • каждая ответственность изолирована;
  • тестирование упрощается;
  • код становится предсказуемым.

Типичные ошибки при использовании Ajv в Express

  • повторная компиляция схем на каждый запрос;
  • отсутствие ограничения additionalProperties;
  • смешивание бизнес-логики и валидации в одном middleware;
  • отсутствие унифицированного формата ошибок;
  • игнорирование params и query, фокус только на body.

Структурирование проекта с валидацией

В крупных проектах удобно выделять слой схем:

/schemas
  user.schema.js
  auth.schema.js
/middlewares
  validate.js
  validateQuery.js
  validateParams.js

Это позволяет:

  • централизовать контроль данных;
  • переиспользовать схемы;
  • поддерживать консистентность API.

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

Использование Ajv в middleware-слое напрямую влияет на безопасность:

  • предотвращает injection через неожиданные поля;
  • защищает от перегрузки payload;
  • ограничивает структуру входных данных;
  • снижает вероятность логических ошибок в бизнес-слое.

Строгая схема становится контрактом между клиентом и сервером, а Express middleware — механизмом принудительного соблюдения этого контракта.