Zod в монорепозиториях используется как слой строгого контракта между пакетами, сервисами и доменными модулями, обеспечивая единообразную валидацию данных на границах системы и синхронизацию runtime-проверок с типами TypeScript. В условиях монорепозитория ключевая ценность Zod проявляется в возможности централизовать схемы и переиспользовать их без дублирования логики валидации.
Монорепозиторий обычно состоит из нескольких пакетов:
frontend, backend, shared,
api, domain, infra. Основная
проблема такой структуры — расхождение контрактов между частями системы.
Типы TypeScript решают только compile-time проверку, тогда как runtime
остаётся незащищённым.
Zod закрывает этот разрыв, превращая схемы в источник истины:
Наиболее распространённый паттерн — выделение отдельного пакета
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,
});
Такой подход снижает связность и позволяет переиспользовать базовые модели.
Монорепозиторий часто объединяет 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-документации и реализации.
При росте системы возникает необходимость эволюции контрактов.
Типичные стратегии:
Добавление новых полей без нарушения совместимости:
z.object({
id: z.string(),
email: z.string(),
nickname: z.string().optional(),
});
export const UserSchemaV1 = z.object({
id: z.string(),
});
export const UserSchemaV2 = UserSchemaV1.extend({
email: z.string(),
});
schemas/
v1/
v2/
Монорепозиторий упрощает поддержку нескольких версий одновременно.
В крупных монорепозиториях важно обеспечить согласованную компиляцию:
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.
В монорепозитории важно избегать падения процесса:
const result = UserSchema.safeParse(input);
if (!result.success) {
logger.error(result.error);
return;
}
Это особенно важно в микросервисных или worker-архитектурах внутри одного репозитория.
Различие подходов:
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 в нейтральный пакетZod может увеличивать размер бандла, если схемы импортируются неправильно.
Рекомендации:
index.ts с re-export всегоimport { UserSchema } from "@repo/schemas/user";
вместо:
import { UserSchema } from "@repo/schemas";
В монорепозитории схемы становятся частью CI pipeline:
.parse() на фикстурахПример теста:
it("validates user payload", () => {
expect(() =>
UserSchema.parse(invalidUser)
).toThrow();
});
В Express, Fastify или tRPC схемы используются как middleware:
app.post("/user", (req, res) => {
const user = UserSchema.parse(req.body);
res.json(user);
});
В monorepo это устраняет необходимость в DTO-классах.
Монорепозитории часто переходят к schema-first подходу:
Это превращает схему в центральный элемент архитектуры.
any вместо z.unknown()packages/
schemas/
user/
order/
contracts/
api/
events/
api/
web/
workers/
Схемы становятся фундаментом взаимодействия всех слоёв системы, обеспечивая единый язык данных между независимыми пакетами и уменьшая вероятность рассинхронизации модели на уровне всей кодовой базы.