REST endpoints

REST endpoints в современных веб-приложениях требуют строгого и предсказуемого контракта между клиентом и сервером. Валидация входящих данных становится критическим слоем архитектуры, особенно при росте количества интеграций и микросервисов. Библиотека Ajv (Another JSON Schema Validator) позволяет формализовать этот контракт через JSON Schema и применять его на уровне HTTP-запросов и ответов.

REST API по своей природе опирается на структуру данных, передаваемых через HTTP. Любой endpoint принимает параметры, тело запроса, query-string и возвращает структурированный JSON. Без формальной схемы эти данные превращаются в неявный контракт, зависящий от документации и договорённостей между командами.

JSON Schema решает эту проблему, описывая:

  • типы данных (string, number, object, array)
  • обязательность полей
  • ограничения (minLength, maximum, pattern)
  • вложенные структуры
  • условную логику валидации

Ajv выступает как высокопроизводительный компилятор этих схем в валидирующие функции.

Базовая настройка Ajv для REST API

При построении REST слоя Ajv обычно инициализируется один раз на уровне приложения:

import Ajv fr om "ajv";

const ajv = new Ajv({
  allErrors: true,
  removeAdditional: "strip",
  coerceTypes: true
});

Ключевые опции:

  • allErrors — возвращает все ошибки, а не первую
  • removeAdditional — удаляет лишние поля, не описанные в схеме
  • coerceTypes — приводит типы (например, “123” → 123)

Эти настройки особенно полезны в REST endpoints, где данные приходят из внешних источников.

Валидация body в POST/PUT endpoints

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

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

const validateUser = ajv.compile(userSchema);

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

app.post("/users", (req, res) => {
  const valid = validateUser(req.body);

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

  // бизнес-логика создания пользователя
  res.status(201).send();
});

Такой подход гарантирует, что бизнес-логика никогда не увидит некорректные данные.

Валидация query parameters

Query-параметры часто недооцениваются, хотя именно они являются источником большого числа ошибок.

const querySchema = {
  type: "object",
  properties: {
    page: { type: "integer", minimum: 1, default: 1 },
    lim it: { type: "integer", minimum: 1, maximum: 100, default: 20 }
  },
  additionalProperties: false
};

const validateQuery = ajv.compile(querySchema);

Применение:

app.get("/products", (req, res) => {
  const valid = validateQuery(req.query);

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

  const { page, limit } = req.query;
});

В сочетании с coerceTypes Ajv автоматически преобразует строки из URL в числа.

Path parameters и их контроль

REST endpoints активно используют параметры маршрута:

const paramsSchema = {
  type: "object",
  required: ["id"],
  properties: {
    id: { type: "string", pattern: "^[0-9a-fA-F]{24}$" }
  }
};

const validateParams = ajv.compile(paramsSchema);

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

app.get("/users/:id", (req, res) => {
  const valid = validateParams(req.params);

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

Это особенно важно при работе с идентификаторами баз данных, такими как MongoDB ObjectId.

Разделение схем по слоям REST API

В сложных системах схемы разделяются по назначению:

  • requestBodySchema — входные данные
  • responseSchema — выходные данные
  • querySchema — параметры запроса
  • paramsSchema — path параметры

Такое разделение позволяет формализовать контракт endpoint полностью.

Пример response schema:

const userResponseSchema = {
  type: "object",
  properties: {
    id: { type: "string" },
    email: { type: "string" },
    createdAt: { type: "string", format: "date-time" }
  },
  required: ["id", "email", "createdAt"]
};

const validateUserResponse = ajv.compile(userResponseSchema);

Хотя валидация ответов используется реже, она критична в микросервисной архитектуре.

Middleware-архитектура для REST endpoints

В реальных приложениях Ajv интегрируется через middleware:

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

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

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

    next();
  };
};

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

app.post("/auth/login", validate(loginSchema), controller);

Такой подход переносит ответственность за валидацию за пределы бизнес-логики.

Расширение Ajv через кастомные форматы

REST API часто требует специфичных проверок:

ajv.addFormat("phone", {
  type: "string",
  validate: (phone) => /^\+?[0-9]{10,15}$/.test(phone)
});

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

const schema = {
  type: "object",
  properties: {
    phone: { type: "string", format: "phone" }
  }
};

Кастомные форматы позволяют стандартизировать доменные правила внутри API.

Кастомные keywords для REST логики

Ajv поддерживает расширение через keywords:

ajv.addKeyword({
  keyword: "isActiveUser",
  validate: (schema, data) => {
    return schema ? data.status === "active" : true;
  }
});

Применение:

const schema = {
  type: "object",
  isActiveUser: true,
  properties: {
    status: { type: "string" }
  }
};

Это позволяет внедрять бизнес-ограничения прямо в схемы REST endpoints.

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

Многие endpoints работают с коллекциями:

const bulkSchema = {
  type: "array",
  items: {
    type: "object",
    required: ["id"],
    properties: {
      id: { type: "string" }
    }
  },
  minItems: 1
};

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

app.post("/users/bulk", (req, res) => {
  const validateBulk = ajv.compile(bulkSchema);

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

Ошибки валидации и структура ответа REST API

Стандартизация ошибок повышает предсказуемость API:

const formatErrors = (errors) => {
  return errors.map(err => ({
    field: err.instancePath,
    message: err.message
  }));
};

Ответ endpoint:

if (!valid) {
  return res.status(400).json({
    message: "Validation error",
    errors: formatErrors(validate.errors)
  });
}

Производительность в REST системах

Ajv компилирует схемы в функции, поэтому важно:

  • компилировать схемы один раз при старте
  • избегать динамической генерации схем внутри запросов
  • переиспользовать валидаторы
const validators = {
  user: ajv.compile(userSchema),
  login: ajv.compile(loginSchema)
};

Интеграция с OpenAPI и контракт REST API

Ajv часто используется вместе с OpenAPI спецификацией. Схемы могут генерироваться из OpenAPI и использоваться для:

  • автоматической валидации запросов
  • тестирования контрактов
  • генерации документации

Это превращает REST endpoints в строго типизированную систему поверх HTTP.

Версионирование схем в REST endpoints

При изменении API важно поддерживать несколько версий схем:

const userSchemaV1 = {...};
const userSchemaV2 = {...};

И маршрутизация:

app.post("/v1/users", validate(userSchemaV1), handlerV1);
app.post("/v2/users", validate(userSchemaV2), handlerV2);

Такой подход снижает риск нарушения обратной совместимости.

Ошибки проектирования REST validation слоя

Распространённые проблемы:

  • дублирование схем между сервисами
  • отсутствие единого реестра валидаторов
  • смешивание бизнес-логики и схем
  • валидация только request, игнорирование response
  • динамическое создание схем в runtime

Корректная архитектура предполагает централизованное управление схемами и их версионирование.

Сочетание Ajv с типизацией

В TypeScript проектах Ajv может дополнять типы:

  • TypeScript гарантирует статическую типизацию
  • Ajv обеспечивает runtime-валидацию

Это особенно важно для REST endpoints, где данные приходят извне и не могут быть доверенными на уровне компиляции.