Представьте экран профиля в мобильном приложении: нужны имя пользователя, аватар и число заказов. В привычном 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на/graphqlHTTP-кэш не работает; кэширование надо продумывать отдельно. - Удаляют поля из схемы сгоряча. Клиент, который его запрашивал, немедленно ломается. Сначала
@deprecated, потом — только когда никто не пользуется. - Тащат GraphQL в простое API «потому что модно», получая сложность резолверов и потерю HTTP-кэша там, где хватило бы REST.
Что учить дальше
GraphQL — это одна развилка в проектировании контракта. Рядом — gRPC для быстрых внутренних вызовов и базовый REST, от которого стоит отталкиваться по умолчанию; отдельная ветка REST — HATEOAS с гиперссылками в ответах. Проблема N+1 и её решения тесно связаны с работой базы данных, а надёжность любых сетевых вызовов — в статье про таймауты, ретраи и идемпотентность. Как выбор стиля вписывается в проектирование системы целиком — в разделе системного дизайна.