Интеграция с ORM (Prisma, TypeORM)

Использование ORM решает задачу абстракции доступа к базе данных, но не решает проблему валидации входных данных. Zod становится промежуточным слоем, который формализует контракт данных между API, бизнес-логикой и уровнем хранения. Основная ценность интеграции заключается в том, что схема данных перестаёт быть разрозненной: одна и та же структура используется для проверки, типизации и частично для документирования поведения системы.


Базовый принцип интеграции

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

  • формирование SQL-запросов;
  • маппинг таблиц на объекты;
  • управление связями;
  • транзакции.

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

  • валидацию входных данных;
  • строгую типизацию на уровне TypeScript;
  • приведение данных к ожидаемой форме;
  • генерацию runtime-ошибок при несоответствии схем.

Ключевая архитектурная идея — валидация всегда выполняется до обращения к ORM.


Prisma и Zod: слой DTO над клиентом

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

Разделение схем create/update

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

Существуют генераторы, которые синхронизируют 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(),
});

Преимущество подхода — устранение дублирования описания структуры данных.


Middleware-валидация Prisma

Prisma поддерживает middleware, что позволяет внедрить валидацию глобально:

prisma.$use(async (params, next) => {
  if (params.model === "User" && params.action === "create") {
    createUserSchema.parse(params.args.data);
  }

  return next(params);
});

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


TypeORM: архитектура через сервисный слой

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

DTO слой с Zod

В 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 и типов ORM

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 применяется на границе системы (controller/service boundary);
  • ORM получает уже нормализованные данные;
  • внутренние доменные операции не валидируются повторно.

Унификация доменной модели

В зрелых архитектурах Zod-схемы становятся частью доменного слоя и используются одновременно для:

  • HTTP DTO;
  • сервисных контрактов;
  • частичной валидации перед ORM;
  • тестирования бизнес-логики.

Prisma или TypeORM в таком подходе выступают лишь адаптерами к базе данных, не содержащими бизнес-правил.