В экосистеме GraphQL типизация на уровне схемы часто создаёт иллюзию полной защищённости данных. Однако схема GraphQL гарантирует только структуру запроса, но не полноценную бизнес-валидацию. На уровне резолверов данные могут требовать дополнительной проверки: диапазоны значений, сложные условия, зависимости между полями.
Zod используется как слой строгой runtime-валидации, который дополняет систему типов GraphQL. Основная идея заключается в том, что схема GraphQL описывает контракт API, а Zod формализует правила корректности данных внутри этого контракта.
Пример базовой валидации аргументов резолвера:
import { z } fr om "zod";
const CreateUserInput = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.number().int().min(18).optional(),
});
type CreateUserInputType = z.infer<typeof CreateUserInput>;
const resolvers = {
Mutation: {
createUser: async (_: unknown, args: unknown) => {
const input = CreateUserInput.parse(args);
return {
id: "1",
email: input.email,
};
},
},
};
Zod в данном контексте выполняет роль строгого фильтра входных данных, предотвращая попадание некорректных значений в бизнес-логику.
GraphQL-схема описывает контракт взаимодействия:
Zod-схемы решают другую задачу:
Пример разделения:
// GraphQL schema (SDL)
type Mutation {
register(input: RegisterInput!): User!
}
input RegisterInput {
email: String!
password: String!
referralCode: String
}
// Zod schema
const RegisterInput = z.object({
email: z.string().email(),
password: z.string().min(10).regex(/[A-Z]/),
referralCode: z.string().optional().transform(val => val?.trim()),
});
GraphQL гарантирует наличие полей, Zod — их корректность.
Одной из ключевых возможностей Zod является трансформация входных данных. В контексте GraphQL это позволяет нормализовать значения ещё до попадания в бизнес-слой.
Пример преобразования:
const PaginationInput = z.object({
lim it: z.string().transform((val) => parseInt(val, 10)).pipe(
z.number().min(1).max(100)
),
offset: z.number().int().nonnegative().default(0),
});
Такой подход особенно полезен при работе с внешними API-клиентами, где типы могут приходить в нестрогом формате.
GraphQL не предоставляет встроенного механизма описания сложных условий между полями. Zod позволяет выразить такие ограничения декларативно.
Пример проверки зависимых полей:
const BookingInput = z.object({
from: z.date(),
to: z.date(),
}).refine((data) => data.to > data.from, {
message: "Дата окончания должна быть позже даты начала",
path: ["to"],
});
В GraphQL-резолвере это используется как единая точка проверки:
const resolvers = {
Mutation: {
createBooking: (_: unknown, args: unknown) => {
const input = BookingInput.parse(args);
return bookingService.create(input);
},
},
};
На практике Zod часто используется как middleware-слой внутри резолверов.
function validate<T>(schema: z.ZodSchema<T>, data: unknown): T {
return schema.parse(data);
}
Использование:
const resolvers = {
Mutation: {
updateProfile: (_: unknown, args: unknown) => {
const input = validate(UpdateProfileSchema, args);
return profileService.update(input);
},
},
};
Такой подход снижает дублирование логики и делает резолверы предсказуемыми.
Одно из ключевых преимуществ Zod — автоматическое выведение типов:
const ProductSchema = z.object({
id: z.string(),
price: z.number(),
title: z.string(),
});
type Product = z.infer<typeof ProductSchema>;
В связке с GraphQL это позволяет синхронизировать:
Особенно полезно при отсутствии codegen-слоя GraphQL.
GraphQL input-типы дублируют структуру Zod-схем, но не заменяют их.
const SearchInput = z.object({
query: z.string().min(2),
tags: z.array(z.string()).optional(),
page: z.number().int().positive().default(1),
});
GraphQL:
input SearchInput {
query: String!
tags: [String!]
page: Int
}
Zod добавляет недостающий слой:
GraphQL-слой не защищает от:
Zod устраняет эти пробелы.
const PaymentSchema = z.object({
amount: z.number().positive(),
currency: z.enum(["USD", "EUR"]),
method: z.enum(["card", "paypal"]),
});
Zod поддерживает композицию схем, что критично для масштабных GraphQL API.
const BaseUser = z.object({
email: z.string().email(),
});
const AdminUser = BaseUser.extend({
role: z.literal("admin"),
permissions: z.array(z.string()),
});
В GraphQL это соответствует расширению типов через интерфейсы, но Zod добавляет runtime-проверку.
Ошибки Zod можно трансформировать в формат GraphQL-ошибок:
import { ZodError } from "zod";
function parseInput(schema: z.ZodSchema, data: unknown) {
try {
return schema.parse(data);
} catch (e) {
if (e instanceof ZodError) {
throw new Error(JSON.stringify(e.flatten()));
}
throw e;
}
}
Это позволяет унифицировать ошибки в GraphQL response layer.
В архитектурах, где GraphQL выступает API-gateway, Zod часто становится слоем DTO:
const UserOutput = z.object({
id: z.string(),
email: z.string().email(),
});
При росте схемы GraphQL появляется проблема:
Zod решает это за счёт:
В крупных системах Zod часто становится источником истины для данных, а GraphQL — лишь транспортным слоем.