Koa валидация

Архитектурный слой валидации в Koa

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

Валидация в Koa обычно охватывает следующие источники данных:

  • тело запроса (request.body)
  • параметры маршрута (request.params)
  • строка запроса (request.query)
  • заголовки (request.headers)

Каждый из этих источников требует отдельной схемы проверки, так как их семантика и структура различаются.


JSON Schema как основа описания данных

Валидация в современных Node.js-приложениях часто строится вокруг JSON Schema — декларативного способа описания структуры данных.

Пример базовой схемы:

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

Ключевые элементы схемы:

  • type — определяет тип структуры
  • required — список обязательных полей
  • properties — описание каждого поля
  • additionalProperties — запрет лишних полей
  • format — дополнительные ограничения (email, uri и др.)

JSON Schema формирует строгий контракт между клиентом и сервером.


Ajv как механизм валидации

Ajv (Another JSON Schema Validator) — высокопроизводительный валидатор JSON Schema для Node.js и браузера. Он компилирует схемы в оптимизированные функции проверки, что делает его одним из самых быстрых решений в своей категории.

Основные особенности Ajv:

  • компиляция схем в JavaScript-функции
  • поддержка JSON Schema draft 7, 2019-09, 2020-12
  • расширяемость через кастомные форматы и ключевые слова
  • поддержка асинхронной валидации
  • строгий контроль ошибок

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

Валидация обычно оформляется как middleware, принимающее схему и проверяющее ctx.request.body.

import Ajv fr om "ajv";

const ajv = new Ajv({ allErrors: true });

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

const validate = ajv.compile(schema);

export function validateBody(ctx, next) {
  const valid = validate(ctx.request.body);

  if (!valid) {
    ctx.status = 400;
    ctx.body = {
      message: "Validation error",
      errors: validate.errors
    };
    return;
  }

  return next();
}

Middleware подключается к маршруту:

router.post("/register", validateBody, async (ctx) => {
  ctx.body = { status: "ok" };
});

Универсальный middleware для схем

Для масштабируемых приложений создаётся фабрика middleware:

const ajv = new Ajv({ allErrors: true });

function validate(schema) {
  const validateFn = ajv.compile(schema);

  return async (ctx, next) => {
    const valid = validateFn(ctx.request.body);

    if (!valid) {
      ctx.status = 400;
      ctx.body = {
        errors: validateFn.errors
      };
      return;
    }

    await next();
  };
}

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

router.post("/login", validate(loginSchema), loginController);
router.post("/users", validate(userSchema), createUserController);

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

Параметры URL требуют отдельной схемы:

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

Middleware:

function validateParams(schema) {
  const validateFn = ajv.compile(schema);

  return async (ctx, next) => {
    const valid = validateFn(ctx.params);

    if (!valid) {
      ctx.status = 400;
      ctx.body = { errors: validateFn.errors };
      return;
    }

    await next();
  };
}

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

Query string часто содержит необязательные поля, поэтому схемы становятся более гибкими:

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

Особенность работы Ajv — возможность использовать default значения через опцию useDefaults:

const ajv = new Ajv({ useDefaults: true });

Форматы и расширение типов данных

Ajv поддерживает встроенные форматы:

  • email
  • uri
  • date-time
  • ipv4 / ipv6

Добавление кастомного формата:

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

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

{
  "type": "string",
  "format": "phone"
}

Кастомные правила валидации

Ajv позволяет расширять систему через keywords:

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

Схема:

{
  "type": "number",
  "isOdd": true
}

Асинхронная валидация

Некоторые проверки требуют обращения к базе данных (например, уникальность email).

const schema = {
  type: "object",
  properties: {
    email: {
      type: "string",
      format: "email",
      async: true,
      validate: async (email) => {
        const user = await db.users.findByEmail(email);
        return !user;
      }
    }
  }
};

Ajv поддерживает async-валидацию при использовании compileAsync:

const validate = await ajv.compileAsync(schema);

const valid = await validate(data);

Стратегии обработки ошибок

Ajv возвращает массив ошибок:

[
  {
    instancePath: "/email",
    message: "must match format \"email\"",
    keyword: "format",
    params: { format: "email" }
  }
]

В Koa ошибки обычно нормализуются:

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

Валидация как слой защиты API

При построении API на Koa валидация выполняет роль первого барьера:

  • предотвращение некорректных данных
  • защита бизнес-логики от неожиданных входов
  • унификация контрактов API
  • снижение количества runtime-ошибок

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


Организация схем в проекте

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

/schemas
  user.schema.js
  auth.schema.js
  product.schema.js

Пример модуля:

export const userSchema = {
  type: "object",
  required: ["name", "email"],
  properties: {
    name: { type: "string", minLength: 2 },
    email: { type: "string", format: "email" }
  }
};

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

Ajv оптимизирует работу за счёт компиляции схем в функции:

  • схема парсится один раз
  • создаётся специализированная функция проверки
  • дальнейшие вызовы не требуют интерпретации JSON Schema

Это критично для Koa-приложений с высокой нагрузкой, где валидация выполняется на каждом запросе.


Типизация и интеграция с TypeScript

При использовании TypeScript схемы могут быть связаны с типами данных:

interface User {
  email: string;
  password: string;
}

Схема и тип синхронизируются вручную или через генерацию типов из JSON Schema, что уменьшает расхождения между контрактом и кодом.


Композиция middleware и порядок выполнения

В Koa порядок middleware имеет значение:

  1. парсинг body
  2. валидация
  3. бизнес-логика

Нарушение порядка приводит к некорректной обработке данных или отсутствию тела запроса в момент валидации.

app.use(bodyParser());
app.use(validate(userSchema));
app.use(controller);