Связка Mongoose и class-validator используется для разделения ответственности между уровнем хранения данных и уровнем валидации бизнес-правил. Mongoose обеспечивает схему и взаимодействие с MongoDB, тогда как class-validator предоставляет декларативную систему проверок на основе классов и декораторов.
Основная идея интеграции заключается в том, что документ Mongoose не обязан отвечать за сложную валидацию входных данных. Вместо этого создаётся отдельный слой DTO (Data Transfer Object), который проходит проверку до попадания в модель базы данных.
Mongoose-валидация ограничена возможностями схемы:
class-validator расширяет эти возможности:
Такое разделение позволяет избегать перегруженных Mongoose-схем, содержащих бизнес-логику.
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 перед созданием документа.
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-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;
}
Без преобразования вложенная валидация не будет работать корректно.
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-структуру.
Одна из ключевых практик — избегать зеркального копирования DTO и Mongoose schema.
// 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);
}
}
Преимущество такого подхода:
Одной из частых проблем является расхождение между DTO и Mongoose schema.
Типичная ситуация:
Решение заключается в строгом разделении ролей:
Полное объединение этих слоёв приводит к потере гибкости и усложнению поддержки.
При обновлениях требуется отдельный DTO, так как правила становятся опциональными.
import { IsOptional, IsString } from "class-validator";
export class UpdateUserDto {
@IsOptional()
@IsString()
username?: string;
@IsOptional()
@IsString()
displayName?: string;
}
Mongoose update операции не всегда проходят через validate hook, поэтому ручная проверка DTO остаётся основным механизмом контроля.
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 это позволяет унифицировать формат ошибок между слоями.
В реальных проектах важны дополнительные аспекты:
Оптимизация часто включает:
Наиболее устойчивой архитектурой является комбинация:
Такое разделение снижает связанность и делает систему предсказуемой при росте модели данных.