Koa не предоставляет встроенного механизма валидации входящих
HTTP-запросов, поэтому проверка данных обычно выносится в отдельный
слой. При использовании class-validator структура
приложения начинает строиться вокруг DTO-классов (Data Transfer
Objects), которые описывают форму входных данных, а middleware Koa
отвечает за преобразование запроса и запуск процесса валидации.
Валидация в Koa при интеграции с class-validator
опирается на три ключевых компонента:
Такой подход отделяет бизнес-логику от проверки данных и делает код более предсказуемым.
Для работы используется связка библиотек:
npm install class-validator class-transformer koa koa-router koa-bodyparser
koa-bodyparser необходим для разбора тела запроса,
поскольку Koa по умолчанию не парсит JSON.
DTO описывает правила проверки входных данных через декораторы:
import { IsEmail, IsString, MinLength, IsInt, Min } fr om "class-validator";
export class CreateUserDto {
@IsEmail()
email;
@IsString()
@MinLength(6)
password;
@IsString()
name;
@IsInt()
@Min(0)
age;
}
Каждое поле снабжается набором ограничений, которые будут проверяться во время валидации.
class-validator работает с экземплярами классов, поэтому
входящий объект должен быть преобразован:
import { plainToInstance } from "class-transformer";
Без этого шага валидация не будет корректно обрабатывать декораторы.
Основной слой интеграции реализуется через middleware-фабрику. Она принимает DTO-класс и возвращает функцию Koa:
import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";
export const validateBody = (DtoClass) => {
return async (ctx, next) => {
const instance = plainToInstance(DtoClass, ctx.request.body);
const errors = await validate(instance, {
whitelist: true,
forbidNonWhitelisted: true,
});
if (errors.length > 0) {
ctx.status = 400;
ctx.body = {
message: "Validation failed",
errors: errors.map(e => ({
property: e.property,
constraints: e.constraints,
})),
};
return;
}
ctx.request.validatedBody = instance;
await next();
};
};
Ключевые параметры validate:
whitelist: true — удаляет поля, не описанные в DTOforbidNonWhitelisted: true — вызывает ошибку при
наличии лишних полейИнтеграция с koa-router выглядит следующим образом:
import Router from "koa-router";
import bodyParser from "koa-bodyparser";
import { validateBody } from "./middlewares/validateBody.js";
import { CreateUserDto } from "./dto/CreateUserDto.js";
const router = new Router();
router.post(
"/users",
bodyParser(),
validateBody(CreateUserDto),
async (ctx) => {
const data = ctx.request.validatedBody;
ctx.body = {
message: "User created",
user: data,
};
}
);
export default router;
Middleware выполняется последовательно: сначала парсинг тела, затем валидация, затем бизнес-логика.
Вместо обработки ошибок в каждом middleware можно использовать единый error handler Koa:
export const errorHandler = async (ctx, next) => {
try {
await next();
} catch (err) {
if (err.name === "ValidationError") {
ctx.status = 400;
ctx.body = {
message: "Validation error",
details: err.errors,
};
return;
}
ctx.status = 500;
ctx.body = {
message: "Internal server error",
};
}
};
Такой подход полезен при расширении логики валидации, когда ошибки начинают выбрасываться через исключения.
Аналогичный подход применяется для ctx.query, но с
отдельным DTO:
import { IsOptional, IsInt } from "class-validator";
export class GetUsersQueryDto {
@IsOptional()
@IsInt()
lim it;
@IsOptional()
@IsInt()
offset;
}
Middleware:
export const validateQuery = (DtoClass) => {
return async (ctx, next) => {
const instance = plainToInstance(DtoClass, ctx.query);
const errors = await validate(instance);
if (errors.length > 0) {
ctx.status = 400;
ctx.body = { errors };
return;
}
ctx.request.validatedQuery = instance;
await next();
};
};
В реальных приложениях часто используется несколько DTO для одного endpoint:
body)query)params)Пример DTO для params:
import { IsUUID } fr om "class-validator";
export class UserParamsDto {
@IsUUID()
id;
}
Middleware аналогично адаптируется:
export const validateParams = (DtoClass) => {
return async (ctx, next) => {
const instance = plainToInstance(DtoClass, ctx.params);
const errors = await validate(instance);
if (errors.length > 0) {
ctx.status = 400;
ctx.body = { errors };
return;
}
ctx.request.validatedParams = instance;
await next();
};
};
Koa позволяет комбинировать несколько уровней валидации:
router.post(
"/users/:id",
bodyParser(),
validateParams(UserParamsDto),
validateBody(UpdateUserDto),
async (ctx) => {
const { validatedParams, validatedBody } = ctx.request;
ctx.body = {
id: validatedParams.id,
update: validatedBody,
};
}
);
Такой подход делает endpoint строго типизированным на уровне исполнения.
HTTP-запросы передают все значения как строки, поэтому без
преобразования class-validator может работать некорректно.
Для этого используется class-transformer:
import { Type } from "class-transformer";
import { IsInt } from "class-validator";
export class PaginationDto {
@Type(() => Number)
@IsInt()
page;
@Type(() => Number)
@IsInt()
lim it;
}
Без @Type(() => Number) значения останутся строками и
проверки типов будут давать неожиданный результат.
Для уменьшения дублирования middleware часто создаются универсальные фабрики:
export const createValidationMiddleware = (source, DtoClass) => {
return async (ctx, next) => {
const instance = plainToInstance(DtoClass, ctx[source]);
const errors = await validate(instance, {
whitelist: true,
transform: true,
});
if (errors.length) {
ctx.status = 400;
ctx.body = {
errors: errors.map(e => ({
field: e.property,
constraints: e.constraints,
})),
};
return;
}
ctx.request.validated = instance;
await next();
};
};
Использование:
validateBody(CreateUserDto)
createValidationMiddleware("query", GetUsersQueryDto)
createValidationMiddleware("params", UserParamsDto)
После валидации данные обычно передаются в сервисы без дополнительной проверки:
class UserService {
async createUser(data) {
return database.users.insert(data);
}
}
Контроллер в Koa остаётся максимально тонким:
router.post("/users", bodyParser(), validateBody(CreateUserDto), async (ctx) => {
const user = await userService.createUser(ctx.request.validatedBody);
ctx.body = user;
});
class-validator поддерживает вложенные объекты, но
требует дополнительной настройки:
import { ValidateNested } from "class-validator";
import { Type } from "class-transformer";
class AddressDto {
@IsString()
city;
@IsString()
street;
}
export class CreateProfileDto {
@IsString()
username;
@ValidateNested()
@Type(() => AddressDto)
address;
}
Без @ValidateNested вложенные правила не будут
применяться.
Koa использует единый объект ctx, поэтому важно избегать
конфликтов между middleware. Обычно валидированные данные помещаются в
отдельные поля:
ctx.request.validatedBodyctx.request.validatedQueryctx.request.validatedParamsТакое разделение предотвращает перезапись исходных данных и сохраняет прозрачность обработки запроса.