Middleware в серверных JavaScript-приложениях выступает промежуточным слоем, который получает управление между получением HTTP-запроса и его обработкой конечным обработчиком. В контексте валидации данных этот слой позволяет централизованно проверять входящие данные до попадания их в бизнес-логику, снижая дублирование кода и повышая предсказуемость обработки запросов.
В экосистеме TypeScript и Node.js одним из наиболее устойчивых решений для декларативной валидации считается class-validator. В связке с ним часто используется class-transformer, обеспечивающий преобразование plain-объектов в экземпляры классов, что критично для корректной работы декораторов валидации.
Middleware для валидации выполняет несколько ключевых функций:
body, query,
params)Такой подход отделяет инфраструктурную логику от бизнес-логики и позволяет контролировать корректность данных на самом раннем этапе обработки запроса.
Библиотека работает на основе декораторов, которые навешиваются на свойства классов:
@IsString()@IsNumber()@IsEmail()@IsOptional()@ValidateNested()Каждый декоратор добавляет метаданные, которые затем используются
функцией validate() или validateOrReject() для
проверки экземпляра класса.
Ключевой момент: валидация работает только с экземплярами классов, а не с обычными объектами. Поэтому middleware почти всегда включает этап трансформации данных.
В Express middleware представляет собой функцию с сигнатурой:
(req, res, next) => {}
Базовая идея middleware валидации:
req.body в экземпляр этого классаnext()import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";
export function validationMiddleware(dtoClass) {
return async (req, res, next) => {
const instance = plainToInstance(dtoClass, req.body);
const errors = await validate(instance);
if (errors.length > 0) {
return res.status(400).json({
message: "Validation failed",
errors: errors.map(err => ({
property: err.property,
constraints: err.constraints,
})),
});
}
req.body = instance;
next();
};
}
DTO-класс определяет структуру входных данных:
import { IsString, IsEmail, IsOptional } from "class-validator";
export class CreateUserDto {
@IsString()
name;
@IsEmail()
email;
@IsOptional()
@IsString()
nickname;
}
Middleware использует этот класс как контракт, гарантируя, что контроллер получит уже проверенную структуру.
import express from "express";
import { validationMiddleware } from "./validationMiddleware";
import { CreateUserDto } from "./dto/CreateUserDto";
const app = express();
app.post(
"/users",
validationMiddleware(CreateUserDto),
(req, res) => {
res.json({
message: "User created",
data: req.body,
});
}
);
Для сложных структур важно использовать ValidateNested и
Type:
import { Type } from "class-transformer";
import { ValidateNested, IsString } from "class-validator";
class AddressDto {
@IsString()
city;
}
class UserDto {
@IsString()
name;
@ValidateNested()
@Type(() => AddressDto)
address;
}
Middleware должен учитывать глубокую трансформацию:
const instance = plainToInstance(dtoClass, req.body, {
enableImplicitConversion: true,
});
Структура ошибок class-validator содержит вложенные данные, которые требуют нормализации.
function formatErrors(errors) {
return errors.map(error => {
return {
field: error.property,
messages: error.constraints
? Object.values(error.constraints)
: [],
children: error.children?.length
? formatErrors(error.children)
: [],
};
});
}
Такой формат позволяет унифицировать ответ API независимо от глубины структуры.
Middleware может поддерживать валидацию разных источников данных:
req.bodyreq.queryreq.paramsПример универсального middleware:
export function validateRequest(dtoClass, source = "body") {
return async (req, res, next) => {
const instance = plainToInstance(dtoClass, req[source]);
const errors = await validate(instance);
if (errors.length > 0) {
return res.status(400).json({ errors });
}
req[source] = instance;
next();
};
}
Некоторые ограничения требуют обращения к базе данных или внешним сервисам:
import { ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";
@ValidatorConstraint({ async: true })
export class IsEmailUnique implements ValidatorConstraintInterface {
async validate(email) {
const user = await findUserByEmail(email);
return !user;
}
defaultMessage() {
return "Email already exists";
}
}
Middleware не требует изменений, но должен поддерживать await validate() без блокировки потока.
При интенсивной нагрузке важны следующие аспекты:
skipMissingProperties)validateOrReject для быстрого fail-fast
режимаПример оптимизированной валидации:
await validate(instance, {
skipMissingProperties: false,
whitelist: true,
forbidNonWhitelisted: true,
});
В фреймворках вроде NestJS аналог middleware реализуется через pipes, но концептуально сохраняется тот же поток:
Middleware-реализация остаётся полезной в чистом Express-приложении, где отсутствует встроенная DI-система.
Типизированная версия повышает безопасность:
import { Request, Response, NextFunction } from "express";
type ClassConstructor<T> = {
new (): T;
};
export function validationMiddleware<T>(dtoClass: ClassConstructor<T>) {
return async (
req: Request,
res: Response,
next: NextFunction
) => {
const instance = plainToInstance(dtoClass, req.body);
const errors = await validate(instance);
if (errors.length) {
return res.status(400).json(errors);
}
req.body = instance;
next();
};
}
Валидация часто комбинируется с другими слоями:
Порядок критичен: валидация должна происходить после аутентификации, но до бизнес-логики.
app.post(
"/secure-route",
authMiddleware,
validationMiddleware(SecureDto),
handler
);
Для масштабируемых систем middleware часто строится как фабрика с параметрами:
export function createValidationMiddleware(options) {
return (dtoClass) => {
return async (req, res, next) => {
const instance = plainToInstance(dtoClass, req[options.source]);
const errors = await validate(instance, options.validatorOptions);
if (errors.length) {
return res.status(options.errorCode || 400).json(errors);
}
req[options.source] = instance;
next();
};
};
}
Комбинация декораторов позволяет формировать строгие контракты API:
Middleware становится точкой enforcement этих правил, гарантируя, что downstream-логика работает только с валидными данными.