В веб-приложениях валидация данных традиционно разделяется между клиентской и серверной сторонами. Клиентская проверка обеспечивает быстрый отклик интерфейса и снижает количество некорректных запросов, серверная — гарантирует целостность данных и защищает систему от недоверенных входных значений.
Основная сложность возникает не в самой валидации, а в поддержании идентичности правил на обеих сторонах. Любое расхождение приводит к рассинхронизации поведения: данные могут проходить проверку на клиенте и отклоняться на сервере либо наоборот.
Типичные источники несоответствий:
При масштабировании системы проблема усиливается пропорционально количеству полей и форм.
Библиотека Zod позволяет описывать структуру данных как runtime-схему, одновременно извлекая из неё статические типы TypeScript.
Ключевая идея заключается в том, что схема становится универсальным контрактом:
Пример базовой схемы:
import { z } from "zod";
export const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().int().min(18),
});
Из схемы автоматически извлекается тип:
export type User = z.infer<typeof userSchema>;
Таким образом устраняется необходимость вручную поддерживать отдельные DTO.
Наиболее устойчивый подход — вынесение схем в общий пакет:
/packages
/shared
/schemas
user.ts
auth.ts
product.ts
И подключение этого пакета как на клиенте, так и на сервере.
Такой подход устраняет дублирование и обеспечивает синхронное обновление правил валидации.
На клиенте схемы используются для предварительной проверки форм и состояния UI.
const result = userSchema.safeParse(formData);
if (!result.success) {
console.log(result.error.format());
}
Метод safeParse позволяет избежать исключений и работать
с результатом как с объектом состояния.
Дополнительно используется refine для бизнес-логики:
const passwordSchema = z.string().min(8).refine(val => {
return /[A-Z]/.test(val);
});
Клиентская валидация не рассматривается как защита, а лишь как улучшение UX.
На сервере схема используется как обязательный фильтр входных данных.
app.post("/users", (req, res) => {
const parsed = userSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json(parsed.error.format());
}
const user = parsed.data;
// дальнейшая обработка
});
Любое отклонение данных блокируется до попадания в бизнес-логику.
Zod формирует структурированный объект ошибок, который может быть приведён к единому API-формату.
const formatZodError = (error: z.ZodError) => {
return error.issues.map(issue => ({
path: issue.path.join("."),
message: issue.message,
}));
};
Это позволяет клиенту отображать ошибки без дополнительной интерпретации.
Сильная сторона Zod заключается в том, что типы выводятся автоматически:
type UserInput = z.input<typeof userSchema>;
type UserOutput = z.output<typeof userSchema>;
Разделение input/output типов важно при использовании трансформаций.
Схемы могут не только проверять, но и преобразовывать данные:
const schema = z.object({
price: z.string().transform(val => Number(val)),
});
Это критично при работе с формами, где всё приходит в виде строк.
Для различных сценариев используются производные схемы:
const updateUserSchema = userSchema.partial();
const publicUserSchema = userSchema.omit({ email: true });
const authSchema = userSchema.pick({ email: true });
Это позволяет переиспользовать структуру без дублирования.
TypeScript обеспечивает только статическую проверку. Runtime-данные остаются недоверенными.
Zod закрывает этот разрыв, обеспечивая:
Передача данных через JSON накладывает ограничения:
Zod позволяет компенсировать это через preprocess:
const schema = z.object({
createdAt: z.preprocess(val => new Date(val as string), z.date()),
});
В REST или RPC слоях схемы становятся контрактом эндпоинта.
Пример с Express:
app.post("/login", (req, res) => {
const result = loginSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json(result.error.format());
}
authenticate(result.data);
});
В Next.js API routes аналогичный подход:
export default function handler(req, res) {
const parsed = schema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json(parsed.error.format());
}
res.json({ ok: true });
}
UI-слой часто требует привязки ошибок к полям формы. Для этого используется нормализация:
const fieldErrors = Object.fromEntries(
error.issues.map(i => [i.path[0], i.message])
);
Такой формат позволяет напрямую интегрировать ошибки в state менеджеры.
При изменении структуры данных возникает необходимость версионирования.
Подходы:
userSchemaV2);extend:const userV2 = userSchema.extend({
nickname: z.string(),
});
optional и
default.Схемы можно логически разделять:
const createUserInput = z.object({...});
const userDomain = createUserInput.extend({ id: z.string() });
const userResponse = userDomain.omit({ password: true });
Синхронизация достигается за счёт:
merge, extend,
intersection.Схемы рассматриваются как самостоятельные единицы логики.
Пример теста:
expect(userSchema.safeParse({ email: "bad" }).success).toBe(false);
Тестирование позволяет фиксировать контракт независимо от UI и API.
Серверная проверка через Zod снижает риск:
Однако схемы не заменяют бизнес-валидацию, а дополняют её.
Чрезмерное усложнение схем приводит к:
Поэтому сложные проверки часто выносятся в отдельные функции,
используемые внутри refine.
Использование Zod формирует единый контур данных:
Такой подход устраняет необходимость ручной синхронизации валидационных правил между частями системы и снижает вероятность расхождений поведения на разных уровнях приложения.