GraphQL inputs

GraphQL-запросы строятся вокруг строгой схемы типов, однако типизация GraphQL не заменяет полноценную валидацию входных данных на уровне бизнес-логики. В сложных приложениях входные аргументы часто включают вложенные структуры, фильтры, сортировки, диапазоны дат и динамические параметры, которые требуют дополнительной проверки. Библиотека Joi предоставляет декларативный способ описания правил валидации и становится удобным инструментом для обработки input-слоёв GraphQL.

Роль Joi в архитектуре GraphQL

GraphQL гарантирует соответствие типов на уровне схемы, но не решает задачи:

  • ограничения диапазонов чисел и строк;
  • проверки бизнес-правил;
  • валидации взаимозависимых полей;
  • нормализации входных данных;
  • защиты от неконсистентных запросов.

Joi применяется в резолверах как дополнительный слой, который отделяет транспортную схему GraphQL от логики домена.

Типичная архитектура включает:

  • GraphQL schema — описание контрактов API;
  • resolvers — обработка запросов;
  • Joi schemas — валидация входных аргументов до выполнения бизнес-логики.

Базовая модель валидации входных аргументов

GraphQL input-объекты обычно соответствуют объектам JavaScript, которые приходят в аргументах резолвера.

Пример входного типа:

input CreateUserInput {
  email: String!
  password: String!
  age: Int
}

Эквивалентная схема Joi:

import Joi fr om 'joi';

const createUserSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).max(64).required(),
  age: Joi.number().integer().min(0).max(120).optional()
});

В резолвере:

const resolvers = {
  Mutation: {
    createUser: async (_, args) => {
      const { error, value } = createUserSchema.validate(args.input);

      if (error) {
        throw new Error(error.details[0].message);
      }

      return userService.create(value);
    }
  }
};

Валидация вложенных GraphQL input-структур

GraphQL активно использует вложенные input-типы, особенно в фильтрации и сложных мутациях.

Пример:

input AddressInput {
  city: String!
  zip: String!
}

input UserProfileInput {
  name: String!
  address: AddressInput
}

Joi позволяет зеркально описывать структуру:

const addressSchema = Joi.object({
  city: Joi.string().required(),
  zip: Joi.string().pattern(/^\d{5}$/).required()
});

const userProfileSchema = Joi.object({
  name: Joi.string().min(2).required(),
  address: addressSchema.required()
});

Особенность вложенных схем заключается в возможности переиспользования и композиции, что особенно важно при росте количества input-типа в GraphQL API.

Массивы и списочные аргументы

GraphQL часто передаёт списки идентификаторов или сложные фильтры.

Пример:

input UsersFilter {
  ids: [ID!]
  roles: [String!]
}

Joi-эквивалент:

const usersFilterSchema = Joi.object({
  ids: Joi.array().items(Joi.string().uuid()).min(1).optional(),
  roles: Joi.array().items(Joi.string().valid('admin', 'user', 'moderator'))
});

Механизм .items() позволяет точно контролировать структуру каждого элемента массива, включая вложенные объекты.

Условная валидация и зависимые поля

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

  • либо email, либо phone;
  • startDate не может быть больше endDate;
  • обязательность поля зависит от другого поля.

Joi поддерживает условную логику через when:

const schema = Joi.object({
  email: Joi.string().email(),
  phone: Joi.string().pattern(/^\+?\d+$/),

  contactType: Joi.string().valid('email', 'phone').required()
}).or('email', 'phone');

Более сложные зависимости:

const periodSchema = Joi.object({
  startDate: Joi.date().required(),
  endDate: Joi.date().greater(Joi.ref('startDate')).required()
});

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

Нормализация данных GraphQL input

GraphQL-клиенты могут отправлять данные в различных форматах. Joi способен не только валидировать, но и нормализовать вход:

const schema = Joi.object({
  email: Joi.string().email().lowercase().trim(),
  tags: Joi.array().items(Joi.string().trim().lowercase())
});

Нормализация снижает нагрузку на бизнес-логику и предотвращает дублирование обработки данных в разных частях системы.

Интеграция Joi в middleware GraphQL

При масштабировании проекта валидацию часто выносят из резолверов в отдельный слой.

Пример middleware-подхода:

const validate = (schema) => (resolver) => {
  return async (parent, args, context, info) => {
    const { error, value } = schema.validate(args.input);

    if (error) {
      throw new Error(error.details.map(d => d.message).join(', '));
    }

    return resolver(parent, { ...args, input: value }, context, info);
  };
};

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

const resolvers = {
  Mutation: {
    createUser: validate(createUserSchema)(async (_, { input }) => {
      return userService.create(input);
    })
  }
};

Такой подход снижает связность кода и стандартизирует обработку ошибок.

Ошибки валидации и форматирование ответа

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

error.details.forEach(d => ({
  message: d.message,
  path: d.path
}));

Эти данные могут быть преобразованы в GraphQL error extensions:

throw new GraphQLError('Validation error', {
  extensions: {
    code: 'BAD_USER_INPUT',
    details: error.details
  }
});

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

Переиспользование схем в GraphQL проектах

В крупных системах схемы Joi становятся частью доменного слоя:

  • userSchemas.js
  • productSchemas.js
  • filterSchemas.js

Композиция:

const basePagination = Joi.object({
  lim it: Joi.number().min(1).max(100).default(20),
  offset: Joi.number().min(0).default(0)
});

const productListSchema = basePagination.keys({
  category: Joi.string().optional()
});

Переиспользование уменьшает дублирование и облегчает сопровождение API.

Безопасность входных данных GraphQL

GraphQL подвержен рискам:

  • чрезмерно глубокие запросы;
  • передача неожиданных структур;
  • попытки обхода бизнес-ограничений.

Joi помогает ограничить поверхность атаки:

const secureSchema = Joi.object({
  query: Joi.string().max(1000).required(),
  depth: Joi.number().max(5)
});

Ограничения глубины и размера входных данных предотвращают перегрузку сервера.

Работа с динамическими фильтрами

GraphQL часто использует гибкие фильтры:

input ProductFilter {
  priceMin: Float
  priceMax: Float
  inStock: Boolean
  search: String
}

Joi:

const productFilterSchema = Joi.object({
  priceMin: Joi.number().min(0),
  priceMax: Joi.number().min(Joi.ref('priceMin')),
  inStock: Joi.boolean(),
  search: Joi.string().max(200)
});

Подобные схемы формируют основу для построения динамических query-builder’ов.

Составные схемы для сложных мутаций

Мутации в GraphQL часто включают несколько уровней вложенности:

input OrderItemInput {
  productId: ID!
  quantity: Int!
}

input CreateOrderInput {
  userId: ID!
  items: [OrderItemInput!]!
}

Joi:

const orderItemSchema = Joi.object({
  productId: Joi.string().required(),
  quantity: Joi.number().integer().min(1).required()
});

const createOrderSchema = Joi.object({
  userId: Joi.string().required(),
  items: Joi.array().items(orderItemSchema).min(1).required()
});

Такая структура обеспечивает строгую проверку целостности данных до выполнения транзакционной логики.

Масштабирование валидации в GraphQL-экосистеме

При росте API становится важным разделение ответственности:

  • schema layer — GraphQL SDL;
  • validation layer — Joi;
  • service layer — бизнес-логика;
  • persistence layer — работа с БД.

Joi в этом контексте выполняет роль формального контракта между API и доменом, обеспечивая предсказуемость входных данных и снижая количество дефектов, связанных с некорректными аргументами запросов.