Работа в монорепозиториях

Zod в монорепозиториях используется как слой строгого контракта между пакетами, сервисами и доменными модулями, обеспечивая единообразную валидацию данных на границах системы и синхронизацию runtime-проверок с типами TypeScript. В условиях монорепозитория ключевая ценность Zod проявляется в возможности централизовать схемы и переиспользовать их без дублирования логики валидации.

Монорепозиторий обычно состоит из нескольких пакетов: frontend, backend, shared, api, domain, infra. Основная проблема такой структуры — расхождение контрактов между частями системы. Типы TypeScript решают только compile-time проверку, тогда как runtime остаётся незащищённым.

Zod закрывает этот разрыв, превращая схемы в источник истины:

  • runtime-валидация входящих данных
  • генерация TypeScript-типов
  • единый контракт между пакетами
  • предотвращение рассинхронизации моделей

Организация пакетов схем

Наиболее распространённый паттерн — выделение отдельного пакета packages/schemas или packages/contracts.

Пример структуры:

monorepo/
  packages/
    api/
    web/
    domain/
    schemas/
    shared/

В пакете schemas размещаются только Zod-схемы без бизнес-логики:

// packages/schemas/user.ts
import { z } from "zod";

export const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  createdAt: z.date(),
});

export type User = z.infer<typeof UserSchema>;

Такой подход превращает пакет в контрактный слой.

Переиспользование схем между пакетами

Монорепозиторий позволяет импортировать схемы напрямую:

// packages/api/src/routes/user.ts
import { UserSchema } from "@repo/schemas/user";

export function parseUser(data: unknown) {
  return UserSchema.parse(data);
}

Ключевой эффект — исключение дублирования валидации на уровне API, frontend и background workers.

Композиция схем и доменная декомпозиция

В крупных системах схемы часто разбиваются по доменам:

schemas/
  user/
    base.ts
    update.ts
    dto.ts
  order/
  product/

Пример композиции:

import { z } from "zod";
import { UserSchema } from "./base";

export const CreateUserSchema = UserSchema.omit({
  id: true,
  createdAt: true,
});

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

Контракты между backend и frontend

Монорепозиторий часто объединяет API и UI. Zod используется как контрактный слой:

// shared/contracts/auth.ts
import { z } from "zod";

export const LoginRequest = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

export const LoginResponse = z.object({
  token: z.string(),
});

Frontend и backend используют один и тот же контракт:

// frontend
import { LoginRequest } from "@repo/schemas/auth";

LoginRequest.parse(formData);
// backend
app.post("/login", (req, res) => {
  const data = LoginRequest.parse(req.body);
});

Это устраняет проблему расхождения API-документации и реализации.

Версионирование схем в монорепозитории

При росте системы возникает необходимость эволюции контрактов.

Типичные стратегии:

1. Additive changes

Добавление новых полей без нарушения совместимости:

z.object({
  id: z.string(),
  email: z.string(),
  nickname: z.string().optional(),
});

2. Версионирование схем

export const UserSchemaV1 = z.object({
  id: z.string(),
});

export const UserSchemaV2 = UserSchemaV1.extend({
  email: z.string(),
});

3. Namespace по версиям

schemas/
  v1/
  v2/

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

Использование TypeScript Project References

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

tsconfig.json
packages/schemas/tsconfig.json
packages/api/tsconfig.json

Схемы становятся источником типов:

export type User = z.infer<typeof UserSchema>;

Это позволяет избежать ручного дублирования интерфейсов.

Валидация границ между пакетами

Монорепозиторий часто страдает от «скрытых зависимостей» между модулями. Zod используется как boundary-layer validation:

// domain -> api boundary
export const DomainEventSchema = z.object({
  type: z.string(),
  payload: z.unknown(),
});

Любая передача данных между пакетами проходит через parse или safeParse.

SafeParse в распределённых системах

В монорепозитории важно избегать падения процесса:

const result = UserSchema.safeParse(input);

if (!result.success) {
  logger.error(result.error);
  return;
}

Это особенно важно в микросервисных или worker-архитектурах внутри одного репозитория.

Общий пакет contracts vs schemas

Различие подходов:

  • schemas — чистая валидация данных
  • contracts — бизнес-ориентированные интерфейсы API

Пример contracts:

export const UpdateProfileContract = z.object({
  userId: z.string(),
  changes: z.object({
    name: z.string().optional(),
    avatarUrl: z.string().url().optional(),
  }),
});

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

Проблемы дублирования и циклических зависимостей

Типичная ошибка монорепозитория — циклические импорты:

api -> schemas -> domain -> api

Решение:

  • отделение schemas в нейтральный пакет
  • запрет импорта бизнес-логики в schemas
  • использование dependency graph linting

Tree-shaking и влияние на бандл

Zod может увеличивать размер бандла, если схемы импортируются неправильно.

Рекомендации:

  • импортировать только нужные схемы
  • избегать index.ts с re-export всего
  • использовать ESM-сборку
import { UserSchema } from "@repo/schemas/user";

вместо:

import { UserSchema } from "@repo/schemas";

CI-проверки схем

В монорепозитории схемы становятся частью CI pipeline:

  • проверка совместимости типов
  • тестирование .parse() на фикстурах
  • snapshot тестирование ошибок Zod

Пример теста:

it("validates user payload", () => {
  expect(() =>
    UserSchema.parse(invalidUser)
  ).toThrow();
});

Интеграция с API-слоем

В Express, Fastify или tRPC схемы используются как middleware:

app.post("/user", (req, res) => {
  const user = UserSchema.parse(req.body);
  res.json(user);
});

В monorepo это устраняет необходимость в DTO-классах.

Паттерн schema-first разработки

Монорепозитории часто переходят к schema-first подходу:

  1. Сначала определяется Zod-схема
  2. Генерируется тип
  3. Используется в API и UI
  4. Добавляется тестирование контрактов

Это превращает схему в центральный элемент архитектуры.

Антипаттерны

  • размещение схем внутри UI-компонентов
  • дублирование одинаковых схем в разных пакетах
  • использование any вместо z.unknown()
  • смешивание бизнес-логики и валидации
  • отсутствие разделения public/private схем

Пример зрелой структуры монорепозитория

packages/
  schemas/
    user/
    order/
  contracts/
    api/
    events/
  api/
  web/
  workers/

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