← назад к разделу

Представьте экран профиля в мобильном приложении: нужны имя пользователя, аватар и число заказов. В привычном REST вы дёргаете /users/42 — и получаете всё поле в поле: адрес, телефон, дату регистрации, настройки уведомлений. Половина ответа не нужна, но она приехала по проводу. А чтобы получить заказы, приходится делать ещё один запрос к /users/42/orders. GraphQL появился в Facebook именно из этой боли: пусть клиент сам опишет, какие данные ему нужны, и получит их одним запросом. Разберёмся, как это устроено и чем приходится платить.

Две проблемы REST: over-fetching и under-fetching

У жёстких REST-ответов есть два симметричных недостатка.

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

Under-fetching — пришло недостаточно, и приходится делать несколько запросов. Экран профиля с заказами — это /users/42, потом /users/42/orders, потом, может, /orders/{id}/items для каждого заказа. Так рождается «водопад» запросов: каждый следующий ждёт предыдущего, и экран собирается медленно.

REST борется с этим костылями — специальные endpoint'ы «под экран», параметры вроде ?fields=name,avatar. Работает, но контракт начинает обрастать частными случаями. GraphQL решает обе проблемы одним приёмом.

Идея: один endpoint и форма данных

В GraphQL, в отличие от REST, нет множества URL под каждый ресурс. Есть один endpoint (обычно /graphql), и клиент шлёт туда запрос, который описывает форму нужных данных — какие поля и какие вложенные объекты вернуть.

Вы не думаете «какой URL дёрнуть». Вы думаете «какой кусок графа данных мне нужен» и рисуете его прямо в запросе. Сервер возвращает JSON ровно той же формы — ничего лишнего, ничего недостающего.

Схема и резолверы

В основе сервера — схема: строго типизированное описание того, какие данные существуют и как они связаны. Это контракт, аналог .proto в gRPC — источник правды, по которому проверяется каждый запрос.

type User {
  id: ID!
  name: String!
  avatarUrl: String
  orders: [Order!]!
}

type Order {
  id: ID!
  status: String!
  amount: Int!
}

type Query {
  user(id: ID!): User
}

Восклицательный знак ! значит «поле не может быть null». Query — точка входа для чтения.

За каждым полем стоит резолвер — функция, которая знает, как это поле добыть: сходить в базу, вызвать другой сервис, посчитать. Резолвер user достанет пользователя по id; резолвер orders внутри User — подтянет его заказы. Сервер соединяет резолверы по дереву запроса и собирает ответ.

Query, mutation, subscription

В GraphQL три типа операций.

  • Query — чтение. «Дай мне вот эти данные». Не меняет состояние, как GET в REST.
  • Mutation — изменение. Создать заказ, отменить, обновить профиль — всё, что меняет данные.
  • Subscription — подписка на события. Сервер сам присылает обновления, когда что-то произошло (новое сообщение, смена статуса заказа) — обычно поверх WebSocket.

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

Пример: запрос и ответ

Тот самый экран профиля — один запрос вместо трёх:

query {
  user(id: "42") {
    name
    avatarUrl
    orders {
      status
      amount
    }
  }
}

Ответ приходит ровно той же формы — только запрошенные поля:

{
  "data": {
    "user": {
      "name": "Анна",
      "avatarUrl": "https://.../42.png",
      "orders": [
        { "status": "paid", "amount": 1990 },
        { "status": "shipped", "amount": 3500 }
      ]
    }
  }
}

Ни телефона, ни настроек — их никто не просил. И заказы приехали вместе с пользователем, без второго обращения к серверу. Обе боли REST закрыты одним запросом.

Чем платят за гибкость

Гибкость не бесплатна — она сдвигает сложность на сервер. Прежде чем брать GraphQL, стоит понимать цену.

Кэширование сложнее. REST-GET кэшируется прокси и CDN по URL из коробки: одинаковый адрес — одинаковый ответ. В GraphQL всё летит одним POST на один /graphql, тело у всех разное — по URL кэшировать нечего. Приходится кэшировать на уровне приложения или клиента, а это заметно больше работы.

Проблема N+1 на резолверах. Запросили список из 20 пользователей и у каждого — заказы. Наивный резолвер orders сходит в базу 20 раз, по разу на пользователя, — плюс один запрос на сам список. Получаем 21 обращение вместо двух. Стандартное лекарство — dataloader: он собирает все id за один «тик», батчит их в один запрос к базе (WHERE user_id IN (...)) и раскладывает результат обратно. Про природу этой проблемы — в статье про PostgreSQL.

Контроль нагрузки. Раз клиент сам строит запрос, он может построить и очень тяжёлый: глубокую вложенность или список из миллиона элементов. Один кривой запрос способен положить сервер. Поэтому в GraphQL добавляют ограничения — максимальную глубину запроса, лимит сложности, тайм-ауты. В REST такого класса риска почти нет: набор ответов там фиксирован заранее.

Версионирование через эволюцию схемы. В REST плодят /v1, /v2. В GraphQL версий обычно нет: схему развивают на месте — добавляют новые поля, а устаревшие помечают @deprecated, но не удаляют, пока есть клиенты. Это удобно, но требует дисциплины: сломать старый клиент легко, если убрать поле раньше времени.

Где это применяется

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

  • Мобильные и веб-приложения с богатыми экранами, где каждый экран собирает свою комбинацию полей — и хочется избежать «водопада» запросов на медленной сети.
  • Агрегация данных из нескольких источников за одним фасадом: клиент делает один запрос, а сервер сам ходит в разные сервисы и базы.
  • Быстро меняющийся фронтенд: новые экраны просят новые срезы данных, не дожидаясь новых endpoint'ов от бэкенда.

Где GraphQL — не лучший выбор:

  • Простое CRUD-API с предсказуемыми ответами — REST будет проще и дешевле, а бесплатное HTTP-кэширование останется при вас.
  • Внутренние высоконагруженные вызовы между сервисами, где важны скорость и строгий контракт, — там уместнее gRPC.
  • Файлы, выгрузки, потоковая отдача — не сильная сторона GraphQL.

Где спотыкаются начинающие:

  • Забывают про N+1. Схема красивая, запрос элегантный, а под капотом сотни обращений к базе. Dataloader (батчинг) — не опция, а норма для любых списков со вложенностью.
  • Оставляют запросы без ограничений. Без лимита глубины и сложности один тяжёлый клиентский запрос кладёт сервер. Ограничения ставят сразу, а не после первого инцидента.
  • Ждут «бесплатного» кэша как в REST. По одному POST на /graphql HTTP-кэш не работает; кэширование надо продумывать отдельно.
  • Удаляют поля из схемы сгоряча. Клиент, который его запрашивал, немедленно ломается. Сначала @deprecated, потом — только когда никто не пользуется.
  • Тащат GraphQL в простое API «потому что модно», получая сложность резолверов и потерю HTTP-кэша там, где хватило бы REST.

Что учить дальше

GraphQL — это одна развилка в проектировании контракта. Рядом — gRPC для быстрых внутренних вызовов и базовый REST, от которого стоит отталкиваться по умолчанию; отдельная ветка REST — HATEOAS с гиперссылками в ответах. Проблема N+1 и её решения тесно связаны с работой базы данных, а надёжность любых сетевых вызовов — в статье про таймауты, ретраи и идемпотентность. Как выбор стиля вписывается в проектирование системы целиком — в разделе системного дизайна.