Интеграция с Fastify и class-validator строится вокруг расширения стандартного жизненного цикла запросов через хуки валидации и внедрения DTO-ориентированного подхода, при котором входные данные приводятся к классовым структурам и проверяются декларативными правилами.
В основе лежит разделение ответственности:
Fastify не предоставляет встроенного класса валидации через decorators, поэтому интеграция реализуется через:
preValidation hookDTO (Data Transfer Object) задаёт контракт входных данных. Используются декораторы class-validator.
import { IsString, IsInt, Min, Max, IsEmail } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@IsInt()
@Min(18)
@Max(120)
age: number;
}
Ключевой принцип: DTO не содержит логики, только описание ограничений.
class-validator работает через рефлексию метаданных. Проверка выполняется так:
import { validate } from 'class-validator';
const dto = Object.assign(new CreateUserDto(), request.body);
const errors = await validate(dto);
Если errors.length > 0, входные данные считаются
некорректными.
Fastify позволяет перехватывать запрос до выполнения handler.
Базовая схема:
fastify.addHook('preValidation', async (request, reply) => {
// логика валидации
});
Создаётся фабрика, принимающая DTO-класс и возвращающая hook.
import { validate } from 'class-validator';
export function validationPipe(DtoClass: any) {
return async function (request: any, reply: any) {
const instance = Object.assign(new DtoClass(), request.body);
const errors = await validate(instance, {
whitelist: true,
forbidNonWhitelisted: true,
});
if (errors.length > 0) {
reply.code(400).send({
message: 'Validation error',
errors,
});
}
request.body = instance;
};
}
fastify.post(
'/users',
{
preValidation: validationPipe(CreateUserDto),
},
async (request, reply) => {
return {
success: true,
data: request.body,
};
}
);
Fastify разделяет входные данные:
request.bodyrequest.queryrequest.paramsДля каждого типа создаются отдельные DTO.
import { IsOptional, IsString } from 'class-validator';
export class GetUsersQueryDto {
@IsOptional()
@IsString()
role?: string;
}
export function validationPipe(DtoClass: any, source: 'body' | 'query' | 'params' = 'body') {
return async (request: any, reply: any) => {
const data = request[source];
const instance = Object.assign(new DtoClass(), data);
const errors = await validate(instance);
if (errors.length > 0) {
return reply.code(400).send({ errors });
}
request[source] = instance;
};
}
class-validator возвращает вложенную структуру ошибок. Для упрощения используется нормализация.
function formatErrors(errors: any[]) {
return errors.map(err => ({
field: err.property,
constraints: err.constraints,
}));
}
Использование:
if (errors.length > 0) {
return reply.code(400).send({
message: 'Validation failed',
errors: formatErrors(errors),
});
}
Параметры whitelist и forbidNonWhitelisted
позволяют контролировать чистоту входных данных:
whitelist: true — удаляет лишние поляforbidNonWhitelisted: true — выбрасывает ошибку при
неизвестных поляхvalidate(instance, {
whitelist: true,
forbidNonWhitelisted: true,
});
При интеграции часто требуется преобразование типов (string → number).
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
const instance = plainToInstance(CreateUserDto, request.body);
const errors = await validate(instance);
Особенно важно для:
import { ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
class AddressDto {
@IsString()
city: string;
}
class UserDto {
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}
Без @Type вложенные объекты не будут валидироваться
корректно.
class-validator поддерживает асинхронные проверки, например проверку уникальности в базе.
import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
@ValidatorConstraint({ async: true })
export class IsEmailUnique implements ValidatorConstraintInterface {
async validate(email: string) {
const user = await db.users.findByEmail(email);
return !user;
}
}
Использование:
import { Validate } from 'class-validator';
export class CreateUserDto {
@Validate(IsEmailUnique)
email: string;
}
При высокой нагрузке важны следующие аспекты:
В крупных системах используется разделение:
Пример структуры:
/dto
create-user.dto.ts
update-user.dto.ts
/validation
validation-pipe.ts
/hooks
user.hooks.ts
Иногда требуется валидировать несколько частей запроса:
fastify.post('/complex', {
preValidation: [
validationPipe(CreateUserDto, 'body'),
validationPipe(GetUsersQueryDto, 'query')
]
}, handler);
Fastify последовательно выполняет массив hook-ов.
Глобальный обработчик ошибок позволяет унифицировать ответы:
fastify.setErrorHandler((error, request, reply) => {
reply.status(500).send({
message: error.message,
});
});
Ошибки валидации можно централизованно обрабатывать через reply внутри pipe.
Для масштабируемых систем часто вводится абстракция:
createValidationPipe(Dto, options)Такая модель позволяет поддерживать единый контракт между: