Введение

В этой статье мы рассмотрим, что такое GraphQL и для чего он был создан. Разберёмся, какие задачи сложно решить в REST API, и какую альтернативу предлагает GraphQL.

Согласно официальной документации, GraphQL — это язык запросов для API-интерфейсов и среда, в которой они выполняются. С помощью GraphQL можно получить данные из API и передать их в приложение так же, как и с помощью REST-like API, JSON-RPC, SOAP и т. д.

Однако GraphQL приводит API в определённый — графовый — вид, что позволяет довольно гибко обращаться с данными:

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

  • Вложенные структуры описывают сложные многоуровневые объекты в одном запросе;

  • Избыточные данные исключаются клиентом из ответов сервера.

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

Именно для этого Facebook в 2012 году создал GraphQL. Разработчики социальной сети столкнулись с уникальными вызовами: миллиарды пользователей, терабайты данных и множество клиентских приложений — мобильные версии, веб-интерфейс, десктопные приложения — все они отображают одну и ту же информацию, но требуют разные наборы данных.

Более того, одно и то же приложение во множестве случаев предоставляет одну и ту же информацию, но в разных форматах. Например, данные о пользователе отображаются на его странице, а также когда этот пользователь показан в списках участников группы, чьих-то друзей и т. д. Атрибуты для каждого запроса могут быть разными, но объект или ресурс один и тот же. REST-решения перестали справляться с этими задачами в масштабе Facebook. Так появился GraphQL.

В этой статье мы на примере разберём, c какими проблемами в структуре данных мы сталкиваемся в REST-like API и как такие задачи решает GraphQL. А также поверхностно рассмотрим реализацию в коде и поверх HTTP, и основные ограничения GraphQL.

Ограничения REST API

Как упоминалось выше, GraphQL был разработан для обхода некоторых ограничений REST-like API.

Давайте рассмотрим пример с заказом в интернет-магазине. Если мы хотим сделать запрос, который возвращает нам объект заказа, то в REST-like API это будет выглядеть так:

GET /api/orders/ORD-2024-1234 HTTP/1.1
Host: api.example.com
Accept: application/json

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-cache
Date: Sat, 02 Nov 2024 14:30:00 GMT
Content-Length: 541

{
  "status": "success",
  "data": {
    "order": {
      "id": "ORD-2024-1234",
      "client": {
        "id": "CLT-789",
        "name": "Иван Петров",
        "email": "ivan@example.com",
        "phone": "+7 (999) 123-45-67"
      },
      "date": "2024-11-02T14:30:00Z",
      "status": "processing",
      "items": [
        {
          "id": "ITEM-001",
          "name": "Смартфон iPhone 13",
          "quantity": 1,
          "price": "75000.00",
          "subtotal": "75000.00"
        },
        {
          "id": "ITEM-002", 
          "name": "Защитное стекло",
          "quantity": 2,
          "price": "990.50",
          "subtotal": "1981.00"
        }
      ],
      "totalAmount": "76981.00",
      "currency": "RUB"
    }
  }
}

Видно, что ответ довольно объёмный, особенно если учесть, что в заказе может быть больше 10 товаров. В большей части вариантов использования запроса параметр items[] вообще не нужен. Например, когда мы узнаём статус заказа, или когда хотим получить контакты клиента заказа, дату формирования или итоговую сумму заказа. Более того, список товаров в оформленном заказе — довольно статичная информация. И даже когда он нужен, в части случаев он может браться из кэша.

Чтобы сократить объём ответа и упростить логику сервера при сборе данных, мы можем сделать отдельные запросы для получения информации о клиенте и товарах по номеру заказа:

GET /api/orders/ORD-2024-1234/client

GET /api/orders/ORD-2024-1234/items

Тогда, если необходимо получить полную информацию по заказу, нужно сделать сразу 3 запроса. И если у вашего приложения несколько клиентов, и каждому из них нужно своё представление данных о заказе — недовольными останутся все.

Из-за того, что мобильному приложению нужна итоговая сумма и название первого товара в заказе, ему надо делать отдельный запрос. Веб-клиенту нужны дата и статус заказа, названия товаров и телефон клиента — он будет выдергивать по паре полей из каждого запроса и собирать всё это в одну структуру. Внешнему партнеру всегда нужна полная информация о заказе и он вообще не понимает, зачем ему делать эти лишние запросы.

Проблема с пользователями решается разработкой трёх отдельных API, но это требует много ресурсов.

Кроме этого, большое количество запросов генерирует большую нагрузку на ваше приложение, что в случае сложного и разветвлённого API и большого количества пользователей может привести к проблемам с производительностью и отказоустойчивостью.

Итак, мы можем сказать, что при определённых масштабах и специфике домена компании сталкиваются с такими ограничениями REST-like API, как:

  • Over-fetching — получение избыточных данных;

  • Under-fetching — недостаток данных, требующий дополнительных запросов;

  • Множественные запросы для получения связанных данных.

Далее рассмотрим, какое решение этих проблем предлагает GraphQL.

Принцип работы GraphQL

В основе концепции GraphQL лежит довольно очевидное решение — каждый клиент в каждой конкретной ситуации сам выбирает, какой набор данных из объекта он хочет получить. То есть при запросе клиент напрямую указывает, какие поля из объекта order в данный момент ему нужны.

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

query GetOrderForMobile($orderId: ID!) {
  order(id: $orderId) {
    items {
      name
    }
    totalAmount
    currency
  }
}

И такой для него придёт ответ:

{
  "data": {
    "order": {
      "items": [
        {
          "name": "Смартфон iPhone 13"
        },
        {
          "name": "Защитное стекло"
        }
      ],
      "totalAmount": "76981.00",
      "currency": "RUB"
    }
  }
}

А вот запрос веб-клиента:

query GetOrderForWeb($orderId: ID!) {
  order(id: $orderId) {
    id
    date
    status
    client {
      phone
    }
    items {
      name
    }
  }
}

И ответ на него:

{
  "data": {
    "order": {
      "id": "ORD-2024-1234",
      "date": "2024-11-02T14:30:00Z",
      "status": "PROCESSING",
      "client": {
        "phone": "+7 (999) 123-45-67"
      },
      "items": [
        {
          "name": "Смартфон iPhone 13"
        },
        {
          "name": "Защитное стекло"
        }
      ]
    }
  }
}

То есть клиент в своём запросе может указать любой набор полей из схемы и в ответе вернутся только эти поля.

Общая схема в нашем случае будет выглядеть так:

query GetOrder($orderId: ID!) {
  order(id: $orderId) {
    id
    date
    status
    client {
      id
      name
      email
      phone
    }
    items {
      id
      name
      quantity
      price
      subtotal
    }
    totalAmount
    currency
  }
}

Но каждый конкретный запрос будет содержать только тот набор полей из общей схемы, который выберет клиент.

GraphQL-запросы строятся на основе схемы — строгого описания типов данных и связей между ними. Например, OrderClient и Item — это типы, которые определяются в схеме и описывают, какие поля у них есть и какого они типа (например, StringIntFloatID и т.д.). Схему мы подробно рассмотрим в следующей статье цикла.

Все запросы к GraphQL обрабатываются через один эндпоинт, что избавляет от необходимости проектировать и поддерживать множество отдельных маршрутов.

В официальной документации есть «живые» примеры, на которых можно потренироваться писать GraphQL-запросы для получения разных полей одного объекта. Либо это можно сделать в тестовых или открытых GraphQL API.

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

Реализация GraphQL-запроса

Кратко рассмотрим реализацию в коде запросов, возвращающих только те данные, которые указал клиент.

Для получения значений полей из БД в коде приложения на сервере нужны специальные функции — resolvers. Эти функции пишут разработчики API на выбранном ими языке программирования. Далее в примерах мы будем использовать JavaScript, но на практике это может быть любой язык, у которого есть библиотека для работы с GraphQL.

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

Резолверы:

  • Выполняются только для запрошенных полей;

  • Имеют доступ к контексту запроса (авторизация, настройки и т.д.);

  • Могут быть асинхронными;

  • Могут извлекать или обновлять данные из REST API, БД или любого другого сервиса.

Рассмотрим, какие резолверы могут быть разработаны в примере с заказом в интернет-магазине:

1. Root resolver получает основные данные заказа. Результатом его выполнения может быть такая структура:

{
  id: "12345",
  date: "2025-04-28",
  status: "SHIPPED",
  clientId: "C-789",
  itemIds: ["ITEM-001", "ITEM-002"],
  totalAmount: "76981.00",
  currency: "RUB"
}

2. Вложенные резолверы для объектов, которые мы хотим получать только в том случае, если их запросил клиент.

Например, мы можем сделать вложенные резолверы client и items:

client: (parent, args, context, info) => {
      return {
        id: "C-789",
        name: "Иван Петров",
        email: "ivan@example.com",
        phone: "+7 123 456 7890"
      };
    },
items: (parent, args, context, info) => {
      return [
        {
          id: "ITEM-001",
          name: "Смартфон iPhone 13",
          quantity: 1,
          price: "75000.00",
          subtotal: "75000.00"
        },
        {
          id: "ITEM-002",
          name: "Защитное стекло",
          quantity: 2,
          price: "990.50",
          subtotal: "1981.00"
        }
      ];
    }

Резолверы принимают следующие аргументы:

  • parent — весь объект ответа родительского резолвера;

  • args — параметры поля резолвера: сортировка, фильтрация, пагинация;

  • context — контекст выполнения запроса, например, данные текущего авторизованного пользователя или доступ к базе данных;

  • info — информация, специфичная для конкретного поля и относящаяся к текущей операции, используется для сложных запросов.

Вложенные резолверы логично выполнять только в случаях, когда клиент запросил одно из полей, которое не вернулось в ответе корневого резолвера.

То есть для запроса ниже будет вызываться корневой резолвер и вложенный резолвер items:

query GetOrderForMobile($orderId: ID!) {
  order(id: $orderId) {
    items {
      name
    }
    totalAmount
    currency
  }
}

Резолвер client здесь выполняться не будет, так как ни одно из его полей не было запрошено клиентом. Таким образом, не будет ни избыточных данных, ни лишних запросов в БД.

Рис.1 — Схема сбора данных резолверами
Рис.1 — Схема сбора данных резолверами

Если мы знаем, что заказ без имени, телефона или email покупателя запрашивают крайне редко, то мы можем обойтись без отдельного резолвера client и доставать данные о покупателе сразу в корневом резолвере. Также это будет иметь смысл, если нам выгоднее один раз загрузить все данные: например, если запрос для заказа и так их вытягивает из БД, данные можно кешировать и пр.

В GraphQL значение каждого поля в запросе достается с помощью резолверов, но не каждое поле обязательно обрабатывается своим резолвером.

Фактически работает следующий механизм:

  1. Выполняется резолвер родительского объекта.

  2. Если родительский резолвер уже вернул данные, содержащие значения для дочерних полей, то GraphQL сначала попытается испол��зовать эти значения.

  3. Резолвер для конкретного поля вызывается только если:

  • Данные для этого поля не были возвращены родительским резолвером;

  • Для поля явно определён специальный резолвер, который должен выполнить дополнительную логику.

Это называется «механизмом разрешения по умолчанию» (default field resolution) и является важной оптимизацией в GraphQL. То есть при грамотном использовании резолверы оптимизируют нагрузку на БД и не запрашивают ненужные данные.

В следующем разделе мы рассмотрим, как GraphQL работает поверх HTTP.

GraphQL и HTTP

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

Как правило, все GraphQL-запросы отправляются с POST и сейчас станет понятно, почему. Так будет выглядеть GraphQL-запрос от веб-клиента, отправленный через HTTP:

POST /graphql HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
Content-Length: 183

{
  "query": "query GetOrderForWeb($orderId: ID!) { order(id: $orderId) { id date status client { phone } items { name } } }",
  "variables": {
    "orderId": "ORD-2024-1234"
  }
}

Как можно заметить, все параметры и схема ответа отправляются в теле запроса.

В какой-то момент объём запроса может стать проблемой. Несмотря на возможности GraphQL, делать очень длинные схемы со множеством вложенных массивов не рекомендуется.

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

Ограничения GraphQL

Мы уже рассмотрели ряд преимуществ GraphQL, но у этой технологии есть и свои ограничения, которые важно учитывать при проектировании API:

  • N+1 проблема
    При работе с вложенными данными один корневой запрос может породить множество последовательных обращений к базе данных (по одному на каждый элемент списка). Это может серьёзно повлиять на производительность. Поэтому часто необходимо оптимизировать запросы, например, с помощью специальной библиотеки DataLoader или батчинга.

  • Сложность кеширования
    Из-за того, что все GraphQL-запросы отправляются на один эндпоинт, невозможно использовать кеширование на уровне URL, как в REST. Для реализации кеша приходится использовать дополнительные решения: например, прокси, кастомные ключи кеша или клиентские библиотеки вроде Apollo Client с нормализацией.

  • Повышенные риски DoS-атак
    GraphQL позволяет делать очень глубокие и широкие запросы, включая фрагменты и рекурсивные структуры. Без ограничения глубины или сложности запроса можно легко перегрузить сервер. Поэтому стоит применять лимиты (depth, complexity) и валидаторы.

  • Отсутствие встроенной авторизации на уровне схемы
    GraphQL не управляет доступом к данным по умолчанию. Все проверки прав доступа должны быть реализованы вручную в резолверах, что усложняет поддержку и увеличивает риск ошибки.

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

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

Заключение

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

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

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

Об авторе

Татьяна Сальникова

Продуктовый архитектор, автор воркшопов
Ведущий системный аналитик
Создавала системы для ритейла, маркетинга, строительства, финансового сектора. Основные направления: проектирование интеграций, API, архитектуры микросервисов