REST endpoints в современных веб-приложениях требуют строгого и предсказуемого контракта между клиентом и сервером. Валидация входящих данных становится критическим слоем архитектуры, особенно при росте количества интеграций и микросервисов. Библиотека Ajv (Another JSON Schema Validator) позволяет формализовать этот контракт через JSON Schema и применять его на уровне HTTP-запросов и ответов.
REST API по своей природе опирается на структуру данных, передаваемых через HTTP. Любой endpoint принимает параметры, тело запроса, query-string и возвращает структурированный JSON. Без формальной схемы эти данные превращаются в неявный контракт, зависящий от документации и договорённостей между командами.
JSON Schema решает эту проблему, описывая:
Ajv выступает как высокопроизводительный компилятор этих схем в валидирующие функции.
При построении REST слоя Ajv обычно инициализируется один раз на уровне приложения:
import Ajv fr om "ajv";
const ajv = new Ajv({
allErrors: true,
removeAdditional: "strip",
coerceTypes: true
});
Ключевые опции:
Эти настройки особенно полезны в REST 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-параметры часто недооцениваются, хотя именно они являются источником большого числа ошибок.
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 в числа.
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.
В сложных системах схемы разделяются по назначению:
Такое разделение позволяет формализовать контракт 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);
Хотя валидация ответов используется реже, она критична в микросервисной архитектуре.
В реальных приложениях 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);
Такой подход переносит ответственность за валидацию за пределы бизнес-логики.
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.
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.
Многие 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);
}
});
Стандартизация ошибок повышает предсказуемость 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)
});
}
Ajv компилирует схемы в функции, поэтому важно:
const validators = {
user: ajv.compile(userSchema),
login: ajv.compile(loginSchema)
};
Ajv часто используется вместе с OpenAPI спецификацией. Схемы могут генерироваться из OpenAPI и использоваться для:
Это превращает REST endpoints в строго типизированную систему поверх HTTP.
При изменении API важно поддерживать несколько версий схем:
const userSchemaV1 = {...};
const userSchemaV2 = {...};
И маршрутизация:
app.post("/v1/users", validate(userSchemaV1), handlerV1);
app.post("/v2/users", validate(userSchemaV2), handlerV2);
Такой подход снижает риск нарушения обратной совместимости.
Распространённые проблемы:
Корректная архитектура предполагает централизованное управление схемами и их версионирование.
В TypeScript проектах Ajv может дополнять типы:
Это особенно важно для REST endpoints, где данные приходят извне и не могут быть доверенными на уровне компиляции.