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

Связка Mongoose и class-validator используется для разделения ответственности между уровнем хранения данных и уровнем валидации бизнес-правил. Mongoose обеспечивает схему и взаимодействие с MongoDB, тогда как class-validator предоставляет декларативную систему проверок на основе классов и декораторов.

Основная идея интеграции заключается в том, что документ Mongoose не обязан отвечать за сложную валидацию входных данных. Вместо этого создаётся отдельный слой DTO (Data Transfer Object), который проходит проверку до попадания в модель базы данных.


Разделение ответственности между схемой и валидацией

Mongoose-валидация ограничена возможностями схемы:

  • проверка типов
  • required-поля
  • минимальные и максимальные значения
  • простые кастомные валидаторы

class-validator расширяет эти возможности:

  • сложные правила (регулярные выражения, зависимости полей)
  • вложенные структуры
  • условная валидация
  • композиция валидаторов

Такое разделение позволяет избегать перегруженных Mongoose-схем, содержащих бизнес-логику.


DTO как промежуточный слой

DTO-классы выступают входной моделью данных перед сохранением в MongoDB.

import { IsString, IsEmail, IsOptional, MinLength, MaxLength } from "class-validator";

export class CreateUserDto {
  @IsString()
  @MinLength(3)
  username: string;

  @IsEmail()
  email: string;

  @IsOptional()
  @IsString()
  @MaxLength(30)
  displayName?: string;
}

DTO не содержит логики сохранения и не зависит от Mongoose. Его задача — гарантировать корректность входных данных.


Базовая интеграция через validate()

Самый прямой способ интеграции — ручной вызов validate перед созданием документа.

import { validate } from "class-validator";
import { plainToInstance } from "class-transformer";
import { CreateUserDto } from "./dto/create-user.dto";
import { UserModel } from "./models/user.model";

async function createUser(payload: any) {
  const dto = plainToInstance(CreateUserDto, payload);

  const errors = await validate(dto);

  if (errors.length > 0) {
    throw new Error("Validation failed");
  }

  return UserModel.create({
    username: dto.username,
    email: dto.email,
    displayName: dto.displayName,
  });
}

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


Использование class-transformer для подготовки данных

class-validator работает с экземплярами классов, поэтому часто используется class-transformer для преобразования plain objects.

Основные задачи преобразования:

  • приведение типов
  • удаление лишних полей
  • создание вложенных объектов
import { Type } from "class-transformer";
import { IsString, ValidateNested } from "class-validator";

class ProfileDto {
  @IsString()
  bio: string;
}

export class CreateUserDto {
  @IsString()
  username: string;

  @ValidateNested()
  @Type(() => ProfileDto)
  profile: ProfileDto;
}

Без преобразования вложенная валидация не будет работать корректно.


Валидация перед сохранением через middleware Mongoose

Mongoose предоставляет middleware hooks, которые можно использовать для интеграции class-validator на уровне модели.

import mongoose from "mongoose";
import { validate } from "class-validator";
import { plainToInstance } from "class-transformer";
import { CreateUserDto } from "../dto/create-user.dto";

const UserSchema = new mongoose.Schema({
  username: String,
  email: String,
  displayName: String,
});

UserSchema.pre("validate", async function (next) {
  const dto = plainToInstance(CreateUserDto, this.toObject());

  const errors = await validate(dto);

  if (errors.length > 0) {
    return next(new Error("Validation failed"));
  }

  next();
});

export const UserModel = mongoose.model("User", UserSchema);

Такой подход переносит проверку ближе к модели, но сохраняет внешнюю DTO-структуру.


Разделение схемы Mongoose и DTO

Одна из ключевых практик — избегать зеркального копирования DTO и Mongoose schema.

Mongoose schema отвечает за:

  • структуру документа в MongoDB
  • индексы
  • связи (populate, refs)
  • базовую валидацию

DTO отвечает за:

  • входные данные API
  • бизнес-валидацию
  • проверку пользовательского ввода
// Mongoose
const UserSchema = new mongoose.Schema({
  username: String,
  email: String,
  createdAt: { type: Date, default: Date.now },
});
// DTO
export class CreateUserDto {
  @IsString()
  username: string;

  @IsEmail()
  email: string;
}

Валидация вложенных документов

Mongoose часто работает с вложенными структурами, которые требуют отдельной обработки в class-validator.

class AddressDto {
  @IsString()
  city: string;

  @IsString()
  street: string;
}

export class CreateUserDto {
  @IsString()
  username: string;

  @ValidateNested()
  @Type(() => AddressDto)
  address: AddressDto;
}

При передаче в Mongoose вложенные объекты должны быть уже валидированы, иначе MongoDB может сохранить неконсистентные данные.


Кастомные валидаторы и бизнес-логика

class-validator позволяет создавать сложные правила через декораторы ValidatorConstraint.

import {
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments,
  Validate,
} from "class-validator";

@ValidatorConstraint({ name: "isUsernameAvailable", async: true })
class IsUsernameAvailable implements ValidatorConstraintInterface {
  async validate(username: string) {
    const exists = await UserModel.exists({ username });
    return !exists;
  }

  defaultMessage(args: ValidationArguments) {
    return "Username already exists";
  }
}

export class CreateUserDto {
  @Validate(IsUsernameAvailable)
  username: string;
}

Такой подход переносит бизнес-ограничения на уровень DTO, разгружая Mongoose.


Интеграция через сервисный слой

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

class UserService {
  async create(payload: any) {
    const dto = plainToInstance(CreateUserDto, payload);

    const errors = await validate(dto);
    if (errors.length) {
      throw new Error("Invalid data");
    }

    return UserModel.create(dto);
  }
}

Преимущество такого подхода:

  • Mongoose остаётся чистым слоем данных
  • class-validator контролирует входные данные
  • сервис объединяет оба слоя

Проблема дублирования типов

Одной из частых проблем является расхождение между DTO и Mongoose schema.

Типичная ситуация:

  • добавлено поле в schema
  • забыто добавить в DTO

Решение заключается в строгом разделении ролей:

  • schema — источник истины для хранения
  • DTO — источник истины для API-входа

Полное объединение этих слоёв приводит к потере гибкости и усложнению поддержки.


Валидация обновлений (update DTO)

При обновлениях требуется отдельный DTO, так как правила становятся опциональными.

import { IsOptional, IsString } from "class-validator";

export class UpdateUserDto {
  @IsOptional()
  @IsString()
  username?: string;

  @IsOptional()
  @IsString()
  displayName?: string;
}

Mongoose update операции не всегда проходят через validate hook, поэтому ручная проверка DTO остаётся основным механизмом контроля.


Частичная валидация и patch-операции

PATCH-запросы требуют гибкой проверки частичных данных.

const dto = plainToInstance(UpdateUserDto, payload);
await validate(dto, { skipMissingProperties: true });

Такой режим позволяет проверять только переданные поля, игнорируя отсутствующие.


Ошибки валидации и их трансформация

class-validator возвращает структурированный массив ошибок, который часто преобразуется перед отправкой клиенту.

function formatErrors(errors: any[]) {
  return errors.map(err => ({
    field: err.property,
    constraints: err.constraints,
  }));
}

В связке с Mongoose это позволяет унифицировать формат ошибок между слоями.


Производственные особенности интеграции

В реальных проектах важны дополнительные аспекты:

  • асинхронные валидаторы увеличивают latency
  • вложенная валидация требует осторожности с производительностью
  • middleware Mongoose может выполняться несколько раз при update/save
  • validate() не кешируется и вызывается каждый раз заново

Оптимизация часто включает:

  • минимизацию асинхронных проверок
  • перенос части логики в сервисный слой
  • использование индексов MongoDB вместо ручных проверок

Гибридный подход: schema validation + DTO validation

Наиболее устойчивой архитектурой является комбинация:

  • Mongoose schema: структурная целостность
  • class-validator DTO: бизнес-правила
  • сервис: координация процесса

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