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, чтобы «ускорить мобильное приложение». Скорость там упирается в число обращений и объём ответа, и ровно это лечится одним заранее собранным адресом под конкретный экран. Такой адрес пишется за день и не тащит за собой ни нового слоя кеширования, ни нового способа отдавать ошибки.