Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → GraphQL против REST: что выбрать для API

GraphQL против REST: честное сравнение для нового API

Когда GraphQL оправдан: гибкие выборки и один эндпоинт против простоты REST, кеширование, версионирование, безопасность запросов и цена эксплуатации.

Прямой ответ

REST — ресурсные эндпоинты с HTTP-семантикой (GET/POST на /orders/42, коды/кеш/версии по URL — практика); GraphQL — один эндпоинг, клиент запросом описывает нужные поля и связи (query { order(id:42){ status items{ name qty } } } — сервер отдаёт ровно это), схема типизирована и само-документирована (introspection — контракт встроен). Сильная сторона GraphQL: клиентские вариативные выборки — мобильное приложение берёт 3 поля, дашборд — 30, без N REST-эндпоинтов «под каждый экран» и over/under-fetching (классика: REST-клиент тянет жирный ответ ради двух полей или делает 4 запроса вместо одного); одна схема на несколько клиентов — реальный выигрыш при 2+ фронтах с разными аппетитами. Цена GraphQL, о которой говорят реже: HTTP-кеш не работает из коробки (всё POST на один URL — CDN/proxy-кеширование — через persisted queries/GET-кеш-прослойку — REST кешируется «бесплатно» заголовками), безопасность запросов — ваша забота (глубокие/тяжёлые запросы клиента = DoS — depth/complexity-лимиты обязательны — REST-эндпоинт стоит фиксированно), N+1 переезжает в резолверы (запрос «10 заказов с товарами» без dataloader — десятки запросов к БД — проблема знакома, в GraphQL системнее), мутации/ошибки/файлы/стримы — на ваших плечах (в REST — HTTP-стандарт). Правило выбора: публичный API/интеграции/простой продукт — REST (+OpenAPI); много клиентов с разными выборками из одного графа данных (веб+мобильные+партнёрский UI) — GraphQL оправдан; «GraphQL потому что современно» при одном фронтенде и 15 ресурсах — усложнение без выигрыша. Гибрид легитимен: GraphQL-фасад над REST/сервисами для своих фронтов (BFF-паттерн) при внешнем REST.

Модель и разработка: схема, типы, эволюция

Контракт: GraphQL-схема — единый источник (types/query/mutation — SDL; кодогенерация клиентов — TypeScript-типы из схемы — «клиент знает ответ на этапе компиляции» — сильнейший DX-выигрыш); REST-контракт — OpenAPI-спека (контрактная разработка — тоже кодоген, но точность «полей ответа» ниже — схемы примеров против строгого типового графа). Эволюция без версий: GraphQL — депркации полей (@deprecated в схеме — клиенты видят, убирают — версия одна, «v2» не рождается; ломать — нельзя, поля уходят годами дефолтом — схема-аккреция); REST — версии URL/заголовков (версионирование — отдельные жизненные циклы, больше дисциплины); на длинной дистанции с многими клиентами GraphQL-эволюция дешевле — но «мусорные» поля живут вечно (чистка деприкейтов — процесс). Разработка сервера (Python): Strawberry/Graphene/FastAPI-интеграции — резолверы на каждый тип/поле; резолвер — место, где живёт логика выборки — и место, где рождаются N+1 (dataloader-паттерн: батч-загрузка связанных сущностей — обязателен с первого дня — ORM-загрузка та же); документация живая (GraphiQL/Playground — introspection — клиент пробует запросы без чтения вики — REST требует поддержания доков отдельно — Sphinx/MkDocs для REST-руководств остаются). Мутации: mutation { createOrder(...) } — последовательны по спеке (REST POST — аналог), но идемпотентность/ретраи/коды — вне спеки (ключи идемпотентности — своё соглашение — идемпотентность); файлы — multipart-конвенция (не часть ядра спеки); «ошибки» — HTTP 200 + массив errors в теле (клиентская обработка — своя логика; REST — HTTP-коды семантичны из коробки — прокси/мониторинг/ретраи понимают их без вас — недооценённое преимущество).

Производительность, кеш, безопасность

Кеширование — главный скрытый налог GraphQL: HTTP-кеш (прокси/CDN/браузер) заточен под GET-URL — GraphQL POST-на-один-эндпоинт мимо него; решения: persisted queries (зарегистрированные запросы с хешем — GET с id — кешируемы; заодно решают безопасность — исполняются только известные запросы), Apollo/urql-клиентские кеши (нормализация по id — частично снимают проблему на фронте), CDN-прослойки с кешем по хешу запроса; REST GET — кешируется заголовками «бесплатно» (публичные справочники/ленты — REST проще и быстрее на инфраструктуре). Безопасность запросов: клиентский GraphQL-запрос произволен — злой/багованный запрос { orders { items { product { reviews { author { orders {...} } } } } } } — вложенность × объём — сервер лежит; лимиты обязательны: max depth (5–7), complexity/quota-cost на запрос, алиасы-лимиты, таймауты (дисциплина лимитов переносится один в один); persisted-queries-режим (whitelist известных запросов) — радикальное решение для приватных фронтов; REST — стоимость эндпоинта фиксирована кодом — «дорогой запрос» невозможен by design (кроме явных ручек — пагинация лимитируется там же). Производительность сервера: REST-запрос — один предсказуемый путь (оптимизируется под него — индексы/кеш); GraphQL — планирование под граф запросов (dataloader-батчи, кеш схемы, анализ типовых запросов из логов — «какие поля реально запрашивают» — unused-поля умирать); интроспекция в проде — отключать/защищать (карта API для атакующего); логирование — запрос+стоимость+резолвер-тайминги (observability сложнее REST-овской «один путь — одна метрика» — трейсинг по резолверам помогает). Порог команды: GraphQL требует зрелости (схема-дисциплина, dataloader с первого дня, лимиты, клиентский кодоген — иначе «GraphQL как REST с лишними шагами»); REST осваивается за день — время до первого прода-запроса разное в разы.

Практический сценарий: BFF-фасад для трёх фронтов

Исходник: продукт — веб-приложение, два мобильных клиента (iOS/Android) и партнёрский виджет; REST-API 40+ эндпоинтов, из них ~12 — «агрегаты под экран» (/mobile/home-summary, /mobile/product-card-short — серверные ручки под конкретные UI — плодились квартал за кварталом); мобильные жалуются на 3–5 запросов на экран (батарея/латентность), веб — на over-fetch жирных ответов. Решение (осознанное, с ADR): GraphQL-BFF (один фасад над внутренними REST-сервисами — не переписывание бэкендов — резолверы зовут внутренние API с dataloader-батчами); Strawberry + FastAPI (интеграция), схема покрыла 4 домена (каталог/заказы/профиль/контент); безопасность: depth ≤ 6, complexity-квота, persisted queries для всех трёх фронтов (prod исполняет только зарегистрированные — админка-исключения — с отдельной ролью), интроспекция закрыта снаружи; кодоген — TypeScript-типы фронтов из схемы (CI-проверка «схема не сломала типы клиентов» — breaking-изменения видны на PR — CI); внешний партнёрский API остался REST (публичный контракт, кеш CDN на справочниках, документация-портал — переписывать на GraphQL для внешних — цена без выигрыша). Итоги за полгода: мобильные экраны — 1 запрос вместо 3–5 (латентность экрана −40%, серверные «агрегатные» ручки удалены — 12 эндпоинтов минус); веб берёт ровно нужные поля (payload главной −60%); «новое поле в мобильном» — правка запроса клиента, не релиз бэкенда (депрекации держат схему чистой — 6 полей в процессе удаления); издержки подтвердились учебно: без complexity-лимита партнёрский скрипт вложенным запросом положил фасад на стейдже — лимиты включены до прода (проверка нагрузки — locust); N+1 в резолвере заказов без dataloader — p95 ×8 на списочном запросе — найдено трейсингом резолверов, dataloader обязателен в ревью-чек-листе (чек-лист). Вывод команды: GraphQL окупился именно на мульти-клиентском фасаде; внутренние сервис-сервис коммуникации — остались REST+очереди (интеграции).

FAQ

Можно ли GraphQL для внутренних микросервисных вызовов? Технически да, практически редко оправдано: сервис-сервис знают, что им нужно (вариативность выборок — про клиентов, не про бэкенды), зато лимиты/резолверы/overhead — платите; внутренний обмен — REST/gRPC (gRPC для строгих контрактов и скорости) или события (outbox); GraphQL — фасад к клиентам (BFF), не ткань между сервисами.

Правда ли, что GraphQL решает N+1? Наоборот — усугубляет, если не готовиться: REST-эндпоинт пишет разработчик, видящий запрос целиком (joinedload — осознанно); GraphQL-резолвер поля не знает, кто и в каком объёме его позовёт (100 заказов → 100 вызовов резолвера items → 100 запросов к БД); лечение — dataloader (батч+кеш на запрос — обязательная инфраструктура с первого дня), соединения с пагинацией по спеке (Relay-connections — лимиты в графе), мониторинг стоимости запросов; «GraphQL из коробки быстрый» — миф, быстрый — спроектированный GraphQL.

Что с версионированием в GraphQL на практике? Версий схемы нет — есть эволюция: новые поля/типы аддитивны, старые — @deprecated(reason) → наблюдение за использованием (поле-метрики в резолверах — «кто ещё зовёт») → удаление, когда ноль вызовов; на практике схема аккрецирует (десятки деприкейтов — нормальное состояние зрелой схемы); риск — «вечные» поля, которые боятся удалить (процесс чистки — как техдолг — с метриками и решением); REST-версии (v1/v2 параллельно) — другая крайность (двойная поддержка) — ни то ни другое бесплатно не даётся.

GraphQL против REST для маленького проекта на одного разработчика? REST почти всегда: один фронтенд — вариативность выборок не нужна, OpenAPI+кодоген дают типизацию, HTTP-кеш/коды/прокси работают бесплатно, порог входа — день; GraphQL на старте — схема/резолверы/dataloader/лимиты — недели инфраструктуры до первого прода-запроса; осмысленный сценарий раннего GraphQL — «с самого начала веб+два мобильных из одного графа данных» (тогда фасад дешевле трёх наборов REST-ручек); миграция REST→GraphQL позже — возможна и не катастрофична (BFF-фасад поверх существующего REST — сценарий выше) — «запереться в REST навсегда» не грозит.

Источники: graphql.org (learn), спецификация GraphQL October 2021, документация Strawberry — ссылки в шапке.