Интеграция с Fastify

Интеграция с Fastify и class-validator строится вокруг расширения стандартного жизненного цикла запросов через хуки валидации и внедрения DTO-ориентированного подхода, при котором входные данные приводятся к классовым структурам и проверяются декларативными правилами.


В основе лежит разделение ответственности:

  • DTO-классы описывают структуру входных данных
  • class-validator определяет правила проверки через декораторы
  • Fastify управляет жизненным циклом запроса и позволяет внедрять preValidation-логику
  • слой адаптера связывает DTO и механизм валидации

Fastify не предоставляет встроенного класса валидации через decorators, поэтому интеграция реализуется через:

  • preValidation hook
  • кастомный плагин
  • опционально — трансформацию payload в экземпляры классов

Подготовка DTO-слоя

DTO (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

class-validator работает через рефлексию метаданных. Проверка выполняется так:

import { validate } from 'class-validator';

const dto = Object.assign(new CreateUserDto(), request.body);
const errors = await validate(dto);

Если errors.length > 0, входные данные считаются некорректными.


Интеграция через preValidation hook в Fastify

Fastify позволяет перехватывать запрос до выполнения handler.

Базовая схема:

fastify.addHook('preValidation', async (request, reply) => {
  // логика валидации
});

Универсальный плагин валидации DTO

Создаётся фабрика, принимающая 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,
    };
  }
);

Валидация query и params

Fastify разделяет входные данные:

  • request.body
  • request.query
  • request.params

Для каждого типа создаются отдельные DTO.

DTO для query

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,
});

Использование class-transformer для приведения типов

При интеграции часто требуется преобразование типов (string → number).

import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';

const instance = plainToInstance(CreateUserDto, request.body);
const errors = await validate(instance);

Особенно важно для:

  • чисел из query-параметров
  • дат
  • вложенных объектов

Вложенные DTO и рекурсивная валидация

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;
}

Управление производительностью

При высокой нагрузке важны следующие аспекты:

  • кэширование metadata class-validator
  • минимизация создания новых экземпляров DTO
  • использование fastify hooks без лишних аллокаций
  • ограничение глубины вложенной валидации

Паттерн разделения валидаторов

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

  • DTO слой (структура)
  • Validation layer (rules)
  • Transport layer (Fastify hooks)

Пример структуры:

/dto
  create-user.dto.ts
  update-user.dto.ts
/validation
  validation-pipe.ts
/hooks
  user.hooks.ts

Комбинирование нескольких DTO в одном маршруте

Иногда требуется валидировать несколько частей запроса:

fastify.post('/complex', {
  preValidation: [
    validationPipe(CreateUserDto, 'body'),
    validationPipe(GetUsersQueryDto, 'query')
  ]
}, handler);

Fastify последовательно выполняет массив hook-ов.


Обработка ошибок на уровне Fastify

Глобальный обработчик ошибок позволяет унифицировать ответы:

fastify.setErrorHandler((error, request, reply) => {
  reply.status(500).send({
    message: error.message,
  });
});

Ошибки валидации можно централизованно обрабатывать через reply внутри pipe.


Ограничения подхода

  • отсутствует нативная интеграция DTO в Fastify
  • необходимость ручного управления transform-слоем
  • избыточная рефлексия при больших схемах
  • сложность глубокой типизации без дополнительных обёрток

Расширяемая модель интеграции

Для масштабируемых систем часто вводится абстракция:

  • createValidationPipe(Dto, options)
  • автоматическое определение source (body/query/params)
  • интеграция с DI контейнером
  • генерация схем из DTO (для OpenAPI)

Такая модель позволяет поддерживать единый контракт между:

  • транспортным слоем Fastify
  • бизнес-логикой
  • системой валидации class-validator