GraphQL интеграция

В GraphQL-схемах основная ответственность за корректность данных традиционно распределяется между типами схемы и резолверами. Однако на практике одного только механизма типизации GraphQL недостаточно: сложные бизнес-правила, составные структуры и условные ограничения требуют дополнительного слоя валидации. В таких сценариях используется библиотека структурной валидации Superstruct, позволяющая задавать строгие правила описания данных и проверять их на этапе выполнения.

Superstruct работает как декларативный валидатор: структуры описываются как композиция примитивов и правил, после чего используются для проверки входных значений. При интеграции с GraphQL это позволяет централизовать проверку аргументов резолверов и входных DTO без дублирования логики в каждом обработчике.


Базовая модель интеграции с GraphQL

GraphQL резолверы получают входные данные в виде аргументов. Именно этот слой становится точкой интеграции с Superstruct:

import { object, string, number, size, validate } fr om "superstruct";

const CreateUserInput = object({
  name: size(string(), 2, 50),
  email: string(),
  age: number()
});

function createUser(_, args) {
  const [error, value] = validate(args.input, CreateUserInput);

  if (error) {
    throw new Error(error.message);
  }

  return userService.create(value);
}

В этом примере структура CreateUserInput описывает контракт входных данных. GraphQL-схема при этом остаётся источником типов на уровне API, а Superstruct добавляет строгую проверку бизнес-ограничений.


Разделение ответственности между GraphQL и Superstruct

GraphQL выполняет роль контракта передачи данных, но не гарантирует соблюдение сложных ограничений. Например:

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

Superstruct закрывает этот слой, сохраняя GraphQL как транспортную и типовую абстракцию.

Типичная схема разделения:

  • GraphQL schema: определяет форму API
  • Superstruct schema: определяет бизнес-валидацию
  • Resolver: связывает API и бизнес-логику

Интеграция через middleware слой резолверов

В больших проектах повторение валидации в каждом резолвере приводит к дублированию логики. Решение — обобщённый wrapper:

import { validate } from "superstruct";

function withValidation(structure, resolver) {
  return (parent, args, context, info) => {
    const [error, value] = validate(args, structure);

    if (error) {
      throw new Error(error.message);
    }

    return resolver(parent, value, context, info);
  };
}

Использование:

const resolvers = {
  Mutation: {
    createUser: withValidation(CreateUserInput, (_, input) => {
      return userService.create(input);
    })
  }
};

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


Работа с вложенными GraphQL input-типами

GraphQL часто использует вложенные структуры input-типов. Superstruct поддерживает композицию структур, что позволяет зеркально отражать сложность схемы:

import { object, string, number, array } from "superstruct";

const Address = object({
  city: string(),
  street: string()
});

const UserProfile = object({
  name: string(),
  addresses: array(Address),
  age: number()
});

При передаче вложенных объектов из GraphQL структура сохраняется без дополнительной обработки.


Валидация аргументов query и mutation отдельно

В GraphQL различаются типы операций, и для них часто применяются разные правила:

  • Query: более мягкие ограничения, фильтры, пагинация
  • Mutation: строгая валидация данных

Superstruct позволяет разделять схемы:

const UserFilter = object({
  search: string(),
  lim it: number()
});

const CreateUser = object({
  name: string(),
  email: string()
});

И использовать их независимо в резолверах соответствующих операций.


Обработка ошибок в GraphQL-формате

GraphQL ожидает структурированные ошибки. Superstruct возвращает объект ошибки, который можно трансформировать:

function formatValidationError(error) {
  return {
    message: "Validation failed",
    details: error.failures ? error.failures() : []
  };
}

Использование в резолвере:

const [error, value] = validate(input, CreateUser);

if (error) {
  throw new Error(JSON.stringify(formatValidationError(error)));
}

В более зрелых системах ошибки преобразуются в GraphQL-compliant формат через кастомные error классы.


Использование refine для бизнес-логики

Superstruct поддерживает кастомные проверки через refine, что особенно важно при интеграции с GraphQL:

import { string, refine } fr om "superstruct";

const EvenStringLength = refine(string(), "EvenStringLength", (value) => {
  return value.length % 2 === 0;
});

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


Динамическая генерация структур для GraphQL фильтров

В сложных API часто используются динамические фильтры. Superstruct позволяет строить структуры программно:

function createFilterStruct(fields) {
  const shape = {};

  for (const field of fields) {
    shape[field] = string();
  }

  return object(shape);
}

Это особенно полезно при построении универсальных GraphQL фильтров и административных API.


Совместимость с Apollo Server

В контексте Apollo Server интеграция Superstruct реализуется через слой резолверов:

import { ApolloServer } from "@apollo/server";

const server = new ApolloServer({
  typeDefs,
  resolvers
});

Superstruct не вмешивается в lifecycle GraphQL сервера, а работает исключительно на уровне бизнес-валидации входных данных, что делает его совместимым с любым сервером GraphQL.


Типизация и Superstruct в TypeScript-проектах GraphQL

При использовании TypeScript Superstruct может выступать как runtime-валидация поверх compile-time типов:

import { Infer } from "superstruct";

type CreateUser = Infer<typeof CreateUserInput>;

Это создаёт единый источник истины: структура одновременно описывает runtime-валидацию и типы приложения.


Оптимизация валидации в GraphQL резолверах

При высокой нагрузке важно минимизировать стоимость проверки. Superstruct позволяет:

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

Пример оптимизации:

const cachedStruct = CreateUserInput;

function resolver(_, args) {
  const [error, value] = validate(args.input, cachedStruct);
  if (error) throw error;
  return service.create(value);
}

Композиция структур для сложных GraphQL API

Superstruct поддерживает композицию, что особенно полезно для масштабируемых GraphQL API:

const Timestamped = object({
  createdAt: string(),
  updatedAt: string()
});

const User = object({
  name: string(),
  email: string(),
  meta: Timestamped
});

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


Использование optional и default значений в GraphQL аргументах

GraphQL допускает nullable и optional поля, но Superstruct требует явного описания:

import { object, string, optional, defaulted } from "superstruct";

const QueryInput = object({
  search: optional(string()),
  lim it: defaulted(number(), 10)
});

Это устраняет неоднозначность поведения аргументов в резолверах.


Централизация схем валидации для GraphQL домена

При масштабировании GraphQL API структура валидации часто фрагментируется. Superstruct позволяет вынести её в отдельный слой доменной модели:

/domain
  /user
    struct.js
    service.js
  /product
    struct.js

GraphQL слой в этом случае становится тонкой оболочкой над доменной логикой, а Superstruct обеспечивает единые правила данных на уровне всего API.