Алексей Меленчук
вся записка
узел 03 · Обмен
дата
объём 6 мин чтения

REST или GraphQL: что выбрать для API

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

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

Вопрос приходит не из статьи про API, а из конкретного разговора: у нас веб и мобильное приложение, мобильному нужно вдвое меньше полей, и он тащит по сети всё подряд. Дальше кто-то произносит «GraphQL», и решение принимают, не назвав, за что платят.

Разница не в скорости

REST раскладывает предметную область по адресам. /orders/42 — заказ, /orders/42/items — его позиции. Ответ у каждого адреса свой, заданный сервером, и клиент берёт что дают.

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

Отсюда единственное настоящее отличие: кто решает, что окажется в ответе. В REST — сервер, в GraphQL — клиент. Всё остальное, включая разговоры про производительность, следствие этого одного решения.

Что GraphQL действительно решает

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

Связанные сущности за один заход. Заказ, его позиции, товары в позициях и остатки по товарам — четыре обращения в REST, одно в GraphQL. На мобильной сети, где дорога каждая задержка, это заметно.

Развитие схемы без версий в адресе. Поле помечается устаревшим, добавляется новое, старые клиенты продолжают работать. Дисциплина совместимости при этом никуда не девается — но она перестаёт выражаться через /v2/.

Обратите внимание, чего в списке нет. Там нет «быстрее».

Что приходится строить заново

Кеш. В REST кеширование дано даром: адрес — ключ, ETag и Cache-Control работают на любом промежуточном узле, от браузера до CDN. GraphQL ходит POST-запросом на один адрес, и по адресу больше ничего не различить. Кеш переезжает внутрь приложения, где его надо написать и инвалидировать самому.

N+1. Резолвер поля вызывается для каждого объекта в списке. Запрос на сто заказов с автором у каждого честно сходит в базу сто раз, если не поставить пакетную загрузку. Это та же болезнь, что у ORM, только теперь её порождает не ваш код, а форма клиентского запроса — то есть сторона, которую вы не контролируете.

Стоимость запроса. Клиент может попросить друзей друзей друзей. Схема с любым циклом позволяет построить запрос, который положит базу, и это не злой умысел, а обычная невнимательность. Нужен либо предел глубины и сложности, либо переход на заранее зарегистрированные запросы, когда клиент шлёт не текст, а идентификатор.

Коды ответов. Типовая реализация отвечает 200 OK и кладёт ошибки в тело. Всё, что у вас настроено на коды — мониторинг, алерты, логика повторов на балансировщике, — перестаёт видеть отказы. Повторы без бюджета особенно неприятны: клиент не понимает, что ответ был ошибочным.

Права на уровне поля. В REST доступ проверяется на адресе. В GraphQL один запрос собирает данные из десятка резолверов, и проверка уезжает в каждый из них. Забытая проверка не видна на обзоре кода — она видна в утечке.

Миф, который живёт дольше остальных

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

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

Что не меняется от выбора

Совместимость контракта. Отсутствие версии в адресе не отменяет обязанности не ломать старых клиентов: удалённое поле ломает их ровно так же, как удалённый адрес.

Идемпотентность повторов. GraphQL про это молчит, как и REST, и сделать получателя идемпотентным придётся самому.

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

Как выбирать

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

Клиент один, он ваш, вы выкатываете его вместе с сервером — берите REST. Разнообразия, ради которого существует GraphQL, у вас нет, а кеш, коды и права вы получите даром. Проблему лишних полей на мобильной сети дешевле закрыть отдельным адресом под мобильный экран, чем новым протоколом.

Потребителей много, они меняются, часть из них не ваша — GraphQL окупается. Но заложите в план не «поднять сервер GraphQL», а построить заново кеш, ограничение сложности и проверку прав на полях. Это недели, а не вечер.

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

Где я могу ошибаться

Я исхожу из того, что кеш и коды ответов вам важны. Если API внутренний, за ним админка на двадцать человек и ни одного слоя кеширования, половина возражений выше не стоит ничего — тогда берите то, с чем команда быстрее выйдет. Вес аргумента задаёт нагрузка, а не вкус.

Чего я делать не советую: переходить на GraphQL, чтобы «ускорить мобильное приложение». Скорость там упирается в число обращений и объём ответа, и ровно это лечится одним заранее собранным адресом под конкретный экран. Такой адрес пишется за день и не тащит за собой ни нового слоя кеширования, ни нового способа отдавать ошибки.