GraphQL и Zod

Валидация входных данных в GraphQL-резолверах

В экосистеме 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-схема описывает контракт взаимодействия:

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

Zod-схемы решают другую задачу:

  • проверка бизнес-ограничений
  • валидация сложных условий
  • преобразование данных (coercion)
  • защита от некорректных runtime-значений

Пример разделения:

// 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 с GraphQL-резолверами

На практике 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);
    },
  },
};

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


Генерация типов TypeScript из Zod-схем

Одно из ключевых преимуществ Zod — автоматическое выведение типов:

const ProductSchema = z.object({
  id: z.string(),
  price: z.number(),
  title: z.string(),
});

type Product = z.infer<typeof ProductSchema>;

В связке с GraphQL это позволяет синхронизировать:

  • типы резолверов
  • входные аргументы
  • внутренние DTO

Особенно полезно при отсутствии codegen-слоя GraphQL.


Валидация GraphQL input-типов через Zod

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-проверку.


Обработка ошибок в GraphQL через Zod

Ошибки 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.


Использование Zod как слой DTO-валидации

В архитектурах, где GraphQL выступает API-gateway, Zod часто становится слоем DTO:

  • входные данные валидируются через Zod
  • бизнес-логика получает строго типизированные структуры
  • выходные данные также могут проверяться схемами
const UserOutput = z.object({
  id: z.string(),
  email: z.string().email(),
});

Масштабирование схем в больших GraphQL API

При росте схемы GraphQL появляется проблема:

  • типы становятся слишком разрозненными
  • логика валидации дублируется
  • сложность поддержки возрастает

Zod решает это за счёт:

  • централизованных схем
  • композиции
  • повторного использования
  • строгой runtime-проверки

В крупных системах Zod часто становится источником истины для данных, а GraphQL — лишь транспортным слоем.