При использовании TypeORM доменная модель часто совмещает две роли: отображение структуры таблицы и контейнер бизнес-данных, которые проходят проверку перед сохранением. Библиотека class-validator позволяет встроить декларативную валидацию прямо в классы сущностей или DTO, формируя единый слой описания ограничений.
TypeORM предоставляет хуки жизненного цикла
(@BeforeInsert, @BeforeUpdate,
@BeforeRemove), которые естественно сочетаются с механизмом
валидации class-validator через функции validate и
validateOrReject.
Сущность TypeORM представляет собой обычный класс с декораторами
@Entity, @Column, дополненный
валидаторами:
import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate } fr om "typeorm";
import { IsEmail, Length, IsNotEmpty, validateOrReject } from "class-validator";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
@IsNotEmpty()
@Length(2, 50)
name: string;
@Column()
@IsEmail()
email: string;
@BeforeInsert()
@BeforeUpdate()
async validate() {
await validateOrReject(this);
}
}
Такой подход переносит ответственность за корректность данных внутрь модели. Перед вставкой и обновлением автоматически выполняется проверка всех декорированных правил.
validateOrReject использует метаданные, которые
class-validator сохраняет через reflect-metadata. При
вызове происходит:
При использовании внутри @BeforeInsert и
@BeforeUpdate TypeORM прерывает сохранение, если
выбрасывается исключение.
@BeforeInsert()
async beforeInsertHook() {
await validateOrReject(this, {
whitelist: true,
forbidNonWhitelisted: true,
});
}
В ряде архитектурных подходов сущности остаются чистыми, а валидация переносится в DTO слой. Это снижает связанность ORM и бизнес-правил.
import { IsEmail, Length } from "class-validator";
export class CreateUserDto {
@Length(2, 50)
name: string;
@IsEmail()
email: string;
}
Сервисный слой выполняет проверку до передачи данных в репозиторий:
import { validateOrReject } from "class-validator";
async function createUser(dto: CreateUserDto) {
await validateOrReject(dto);
const user = userRepository.create(dto);
return userRepository.save(user);
}
Такой подход отделяет инфраструктуру хранения данных от правил валидации.
Репозиторий можно расширить, добавив централизованную проверку перед сохранением:
import { Repository } from "typeorm";
import { validateOrReject } from "class-validator";
export class UserRepository extends Repository<User> {
async saveWithValidation(entity: User) {
await validateOrReject(entity);
return this.save(entity);
}
}
Этот слой становится точкой контроля, исключающей обход валидации.
class-validator позволяет создавать собственные правила, которые могут учитывать состояние базы данных через dependency injection.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from "class-validator";
import { DataSource } from "typeorm";
import { User } from "./User";
@ValidatorConstraint({ async: true })
export class IsEmailUnique implements ValidatorConstraintInterface {
constructor(private dataSource: DataSource) {}
async validate(email: string) {
const repo = this.dataSource.getRepository(User);
const count = await repo.count({ wh ere: { email } });
return count === 0;
}
defaultMessage(args: ValidationArguments) {
return `Email ${args.value} уже используется`;
}
}
Использование в сущности:
import { Validate } from "class-validator";
@Column()
@Validate(IsEmailUnique)
email: string;
class-validator сам по себе не знает о TypeORM контейнере
зависимостей. Для интеграции используется useContainer:
import { useContainer } from "class-validator";
import { Container } from "typedi";
useContainer(Container);
Это позволяет внедрять сервисы внутрь кастомных валидаторов, включая репозитории и внешние сервисы.
TypeORM часто работает с отношениями OneToMany,
ManyToOne, где требуется рекурсивная проверка:
import { ValidateNested, IsNotEmpty } from "class-validator";
import { Type } from "class-transformer";
export class Address {
@IsNotEmpty()
city: string;
}
@Entity()
export class User {
@ValidateNested()
@Type(() => Address)
address: Address;
}
class-transformer необходим для корректного
преобразования plain objects в классы перед валидацией.
TypeORM поддерживает каскадные операции:
@OneToMany(() => Post, (post) => post.user, { cascade: true })
posts: Post[];
При cascade: true вложенные сущности могут быть
сохранены без явного вызова валидации, если она не встроена в lifecycle
hooks.
Для предотвращения этого часто используется ручная рекурсивная проверка:
@BeforeInsert()
async validateAll() {
await validateOrReject(this);
if (this.posts) {
for (const post of this.posts) {
await validateOrReject(post);
}
}
}
Разные операции требуют разных правил: создание, обновление, частичное обновление.
import { IsEmail, Length } from "class-validator";
export class User {
@Length(2, 50, { groups: ["create", "update"] })
name: string;
@IsEmail({ groups: ["create"] })
email: string;
}
Использование:
await validateOrReject(user, { groups: ["create"] });
В TypeORM это часто применяется в хуках:
@BeforeInsert()
async beforeInsert() {
await validateOrReject(this, { groups: ["create"] });
}
Метод save в TypeORM позволяет выполнять partial update,
что создаёт проблему неполных объектов. class-validator по умолчанию
ожидает полную модель.
Решение заключается в использовании
skipMissingProperties:
await validateOrReject(entity, {
skipMissingProperties: true,
});
Использование валидаторов прямо в сущностях приводит к нескольким системным особенностям:
Часто формируется следующая цепочка:
DTO → validateOrReject → service layer → entity creation → repository save
или
request → transformation (class-transformer) → DTO validation → mapping → TypeORM entity
Такой поток снижает вероятность того, что некорректные данные попадут в persistence layer, независимо от используемого ORM поведения.
При работе с транзакциями TypeORM валидация обычно выполняется до открытия транзакции, чтобы избежать отката из-за ошибок данных:
await validateOrReject(dto);
await dataSource.transaction(async manager => {
const user = manager.create(User, dto);
await manager.save(user);
});
Если же валидация находится внутри entity hooks, она выполняется уже в рамках транзакционного контекста, что может увеличивать стоимость отката.
validateOrReject выбрасывает массив объектов
ValidationError:
[
{
property: "email",
constraints: {
isEmail: "email must be an email"
}
}
]
TypeORM не интерпретирует эти ошибки, поэтому они должны быть обработаны на уровне сервиса или middleware.
При росте модели до десятков полей и связей рекомендуется разделять:
class-validator в этом случае становится инструментом DTO-слоя, а TypeORM остаётся исключительно persistence-механизмом.