Использование ORM решает задачу абстракции доступа к базе данных, но не решает проблему валидации входных данных. Zod становится промежуточным слоем, который формализует контракт данных между API, бизнес-логикой и уровнем хранения. Основная ценность интеграции заключается в том, что схема данных перестаёт быть разрозненной: одна и та же структура используется для проверки, типизации и частично для документирования поведения системы.
ORM отвечает за:
Zod отвечает за:
Ключевая архитектурная идея — валидация всегда выполняется до обращения к ORM.
Prisma генерирует строгие типы на основе схемы
schema.prisma, однако эти типы не защищают от некорректного
пользовательского ввода. Поэтому поверх Prisma обычно строится слой DTO,
основанный на Zod.
import { z } from "zod";
export const createUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(2).optional(),
});
Использование перед вызовом Prisma:
import { prisma } from "./prismaClient";
import { createUserSchema } from "./schemas/user";
async function createUser(input: unknown) {
const data = createUserSchema.parse(input);
return prisma.user.create({
data,
});
}
Prisma различает операции create и update,
что логически отражается в Zod:
export const updateUserSchema = z.object({
email: z.string().email().optional(),
name: z.string().min(2).optional(),
});
Для частичных обновлений часто используется композиция:
const baseUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
});
export const partialUserSchema = baseUserSchema.partial();
Существуют генераторы, которые синхронизируют Prisma и Zod, например
подходы на основе zod-prisma генераторов. Идея заключается
в том, чтобы схема базы данных становилась источником истины:
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
}
Генерация Zod-схем позволяет получить:
export const UserSchema = z.object({
id: z.number(),
email: z.string(),
name: z.string().nullable(),
});
Преимущество подхода — устранение дублирования описания структуры данных.
Prisma поддерживает middleware, что позволяет внедрить валидацию глобально:
prisma.$use(async (params, next) => {
if (params.model === "User" && params.action === "create") {
createUserSchema.parse(params.args.data);
}
return next(params);
});
Такой подход централизует контроль данных, но требует осторожности при масштабировании, так как усложняет трассировку ошибок.
TypeORM использует сущности на основе классов, однако сам ORM не предоставляет механизмов runtime-валидации. Поэтому интеграция с Zod строится через сервисный слой.
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column({ nullable: true })
name?: string;
}
В TypeORM чаще всего используется отдельный слой DTO:
export const createUserDTO = z.object({
email: z.string().email(),
name: z.string().optional(),
});
Сервис:
import { AppDataSource } from "./dataSource";
import { User } from "./entity/User";
async function createUser(input: unknown) {
const data = createUserDTO.parse(input);
const repo = AppDataSource.getRepository(User);
const user = repo.create(data);
return repo.save(user);
}
Для уменьшения дублирования логики часто создаётся обёртка над репозиториями:
class UserRepository {
constructor(private repo = AppDataSource.getRepository(User)) {}
create(input: unknown) {
const data = createUserDTO.parse(input);
return this.repo.save(this.repo.create(data));
}
}
Такой подход делает валидацию неотделимой от операций записи.
Zod позволяет извлекать типы:
type CreateUserInput = z.infer<typeof createUserDTO>;
Prisma уже предоставляет собственные типы:
import { Prisma } from "@prisma/client";
type PrismaUserCreate = Prisma.UserCreateInput;
Расхождение между этими типами часто становится источником ошибок, поэтому распространён подход явного сопоставления:
const data: Prisma.UserCreateInput = createUserDTO.parse(input);
Zod возвращает структурированный ZodError, который
обычно преобразуется в формат ошибок API:
import { ZodError } from "zod";
function formatError(error: unknown) {
if (error instanceof ZodError) {
return {
message: "Validation error",
issues: error.issues,
};
}
throw error;
}
В связке с ORM это важно, так как ошибки базы данных и ошибки валидации должны обрабатываться отдельно.
Prisma поддерживает nested writes:
prisma.user.create({
data: {
email,
profile: {
create: {
bio: "text",
},
},
},
});
Zod позволяет описывать вложенные структуры:
const profileSchema = z.object({
bio: z.string(),
});
const userSchema = z.object({
email: z.string().email(),
profile: profileSchema.optional(),
});
Для сложных доменов используется композиция:
const emailSchema = z.object({
email: z.string().email(),
});
const passwordSchema = z.object({
password: z.string().min(8),
});
const authSchema = emailSchema.merge(passwordSchema);
Для update-операций:
const updateSchema = authSchema.partial();
Zod не заменяет транзакционную логику ORM, но влияет на входные гарантии:
await prisma.$transaction(async (tx) => {
const user = createUserSchema.parse(input);
const created = await tx.user.create({ data: user });
await tx.profile.create({
data: {
userId: created.id,
},
});
});
Валидация до транзакции уменьшает вероятность откатов из-за некорректных данных.
Добавление Zod в слой ORM-персистенции увеличивает количество операций на каждый запрос:
Оптимизационный подход заключается в разделении:
В зрелых архитектурах Zod-схемы становятся частью доменного слоя и используются одновременно для:
Prisma или TypeORM в таком подходе выступают лишь адаптерами к базе данных, не содержащими бизнес-правил.