REST vs RPC vs GraphQL (стили API)

Назначение: систематизация критериев выбора стиля API — REST, RPC/gRPC, GraphQL: какие задачи каждый стиль решает лучше всего, где начинаются его ограничения и как комбинировать стили в одной системе.

Аудитория: разработчики, техлиды, архитекторы.

Статус: устоявшиеся подходы (REST — Рой Филдинг, 2000; XML-RPC — 1998 / JSON-RPC — 2005 / gRPC — Google, 2015; GraphQL — Facebook, 2015).

Не путать с: Интеграционными паттернами — структурными решениями вокруг API (API Gateway, BFF, Service Mesh); Микросервисами — стилем декомпозиции системы, для которого стиль API выбирают отдельно.

Общее

Стиль API — архитектурное решение о том, в каких терминах клиент и сервер договариваются. Клиент либо обращается к ресурсам (REST), либо вызывает процедуры (RPC, включая gRPC), либо запрашивает поля графа данных (GraphQL). Выбор определяет форму контракта между сторонами, способ передачи данных, модель ошибок, возможности кэширования и то, как интерфейс будет меняться со временем. В отличие от выбора архитектурного стиля всей системы — монолит или микросервисы — выбор стиля API затрагивает одну границу «система ↔ потребители», но принимается надолго: от контракта зависят все клиенты, и его смена стоит дороже смены внутренней реализации.

Три семейства подходов отвечают на один и тот же вопрос — «как клиент получает от системы пользу?» — разными грамматиками:

  • Ресурсы. Мир API описан как набор сущностей с адресами; клиент сообщает, что он хочет сделать с ресурсом, выбирая HTTP-метод, а сервер возвращает его представление. Пользователь — не «объект с методами», а ресурс user с адресом.
  • Процедуры. Мир API описан как набор операций; клиент называет, что именно нужно выполнить, передаёт аргументы и получает результат. Сеть скрывается за вызовом, похожим на вызов функции.
  • Граф данных. Мир API описан как связанный граф типизированных объектов; клиент сам перечисляет, какие поля каких объектов ему нужны, а сервер собирает ответ ровно в этой форме.
   Ресурсы (REST)            Процедуры (RPC/gRPC)      Граф данных (GraphQL)
   ──────────────            ──────────────────        ─────────────────────
   GET /users/42             GetUser(GetUserRequest)   query { user(id: 42)
                                                          { name
                                                            orders { total } } }

   «верни представление      «выполни операцию         «верни перечисленные
    ресурса user»             с этими аргументами»      поля этого узла»

Стиль влияет на систему по четырём осям. Контракт: где он живёт — в соглашении об HTTP-методах и кодах, в IDL-файле с кодогенерацией или в схеме с интроспекцией. Производительность: объём служебных данных, число полных сетевых обращений на сценарий, стоимость сериализации. Кэширование: умеет ли промежуточное звено (браузер, прокси) кэшировать ответы без знания приложения. Эволюция: как добавляются поля и операции без поломки существующих клиентов. Практический вопрос выбора почти всегда сводится к характеру доминирующих операций: CRUD над сущностями, действия-команды или сложные выборки данных — у каждого из трёх стилей свой «родной» тип задач.

Историческая линия подходов:

ГодПодходВеха
1998XML-RPCПростейший RPC поверх HTTP с XML-кодированием
1999SOAP, WSDLКорпоративный RPC со строгим контрактом
2000RESTДиссертация Роя Филдинга: ресурсы и единый интерфейс
2005JSON-RPCОблегчённый RPC с JSON вместо XML
2007ThriftКросс-языковой IDL с кодогенерацией (Facebook)
2015gRPCProtobuf поверх HTTP/2 (Google, из внутреннего Stubby)
2015GraphQLСхема и запросы под форму данных (Facebook)

Линия показывает любопытную деталь: RPC как идея старше REST на десятилетия, REST выросла как ответ на сложность корпоративных RPC-стеков, а GraphQL возник как ответ на ограничения REST в задачах агрегации данных для мобильных клиентов. Ни один подход не отменял предыдущие — каждый занял свою нишу, и зрелые системы почти всегда используют два-три стиля одновременно на разных границах.

Место стилей среди соседних решений: интеграционные паттерны (API Gateway, BFF) отвечают за то, через что клиенты входят в систему, — и работают поверх любого стиля; введение в архитектуру задаёт общий контекст решений такого уровня. Синхронный обмен в любом стиле — не единственный способ интеграции: для развязки во времени системы дополняют его асинхронным (EDA, очереди сообщений).

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

REST

REST (Representational State Transfer, «передача репрезентативного состояния») — архитектурный стиль, определённый Роем Филдингом в докторской диссертации (2000) как обобщение принципов, на которых построена Сеть. Ключевые идеи: система экспонирует ресурсы — сущности, у каждого из которых есть адрес (URI); клиент работает с представлениями ресурсов (документы JSON, XML), а не с самими сущностями; взаимодействие строится на едином интерфейсе — фиксированном наборе HTTP-методов с заранее оговорённой семантикой, одинаковой для всех ресурсов всех систем.

Свойства, из которых складывается стиль:

  • Адресация ресурсов. Каждый ресурс имеет стабильный URI: /users/42, /orders/17/items. Адрес — идентичность, а не команда.
  • Единый интерфейс. Семантика методов фиксирована протоколом: GET — прочитать (безопасный и идемпотентный), PUT — заменить целиком (идемпотентный), DELETE — удалить (идемпотентный), POST — создать или выполнить неидемпотентное действие, PATCH — частично изменить (RFC 5789).
  • Отсутствие состояния (statelessness). Каждый запрос самодостаточен: сервер не хранит сессию клиента между запросами, всё нужное — в самом запросе. Это позволяет балансировать нагрузку произвольно и переживать отказы экземпляров.
  • Кэшируемость. Ответы объявляют свою кэшируемость заголовками — промежуточные звенья могут их переиспользовать.
  • HATEOAS. Зрелая форма стиля: ответы содержат гиперссылки на допустимые следующие действия, и клиент движется по приложению, следуя за ссылками, а не по заранее известным адресам.

Типовое взаимодействие:

GET /users/42 HTTP/1.1
Host: api.example.com
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
ETag: "a3f5c2e"

{
  "id": 42,
  "name": "Анна Сергеева",
  "email": "anna@example.com"
}

Частичное обновление с оптимистичной блокировкой — PATCH вместе с условным заголовком If-Match: если представление изменилось с момента чтения, сервер ответит 412 Precondition Failed:

PATCH /users/42 HTTP/1.1
Host: api.example.com
Content-Type: application/merge-patch+json
If-Match: "a3f5c2e"

{ "email": "anna.new@example.com" }

Модель зрелости Ричардсона

Насколько конкретный «REST-подобный» API близок к стилю, показывает модель зрелости Ричардсона (Леонард Ричардсон, 2008) — четырёхуровневая шкала, де-факто стандартная для классификации HTTP API:

УровеньЧто появляетсяКак выглядит API
0—Один адрес, весь обмен — POST с документами (RPC поверх HTTP)
1РесурсыУ сущностей — собственные URI, но действия по-прежнему через один POST
2Методы и коды ответовКорректное использование GET/POST/PUT/DELETE и осмысленных HTTP-кодов
3HATEOASОтветы снабжены ссылками на допустимые следующие действия

Практический вывод из модели: подавляющее большинство «REST API» в промышленности находится на уровне 2 — ресурсы, методы, коды ответов, — и это разумный компромисс: уровень 3 в промышленных объёмах даёт сложность, редко оправданную выгодой. Филдинг, впрочем, настаивал, что без гипермедиа API REST-ом не является; в индустрии за такими интерфейсами закрепилось более точное разговорное имя — «HTTP API». Внешний машиночитаемый контракт в REST — не часть стиля, а надстройка: спецификации OpenAPI (Swagger) описывают адреса и формы данных отдельно от самого кода.

Кэширование и ограничения

Сильная сторона REST — наследуемое у HTTP кэширование: Cache-Control задаёт свежесть ответа, ETag и If-None-Match позволяют серверу ответить 304 Not Modified без тела, Last-Modified покрывает простые случаи. Браузеры, обратные прокси и CDN кэшируют GET-ответы без какого-либо знания о приложении — для читающих API это бесплатная производительность и разгрузка.

Ограничения стиля проявляются там, где данные составные. Избыточная выборка (over-fetching): ответ фиксирован сервером, и клиенту, которому нужно одно поле, прилетает весь документ. Недостаточная выборка (under-fetching): экрану, показывающему пользователя, его заказы и статусы доставки, нужно три-четыре адреса — столько же отдельных сетевых обращений, каждый со своей задержкой и риском отказа. Форма ответа жёстко связана с экраном, поэтому любое изменение потребностей клиентов — это новая версия эндпоинта или размножение специализированных адресов; версионирование (слеш-версии в URI) превращается в постоянную цену эволюции. Наконец, REST слабо выражает действия: операция «отменить заказ» плохо ложится на грамматику «ресурс + метод», и дизайнеры API изобретают полуресурсы вида /orders/17/cancellation.

RPC и gRPC

RPC (Remote Procedure Call, «вызов удалённой процедуры») — стиль, при котором сетевое взаимодействие маскируется под вызов функции: клиент вызывает GetUser(user_id), библиотека сериализует аргументы, передаёт их по сети, сервер выполняет процедуру и возвращает результат. Идея старше веба: Sun RPC (1988) и CORBA (1991) решали ту же задачу; веб-поколение — XML-RPC (1998), SOAP (1999) и JSON-RPC (2005); современное — gRPC (Google, 2015, вырос из внутреннего фреймворка Stubby; с 2017 — проект CNCF), ставший де-факто стандартом межсервисного обмена. Линию продолжают и родственные решения — Thrift, ttrpc, connect, — но принципы у семейства общие, поэтому далее оно описывается на примере gRPC.

gRPC собирается из трёх частей. Protobuf — язык описания интерфейсов (IDL) и бинарный формат сериализации: контракт пишется в .proto-файле, из которого генерируется типизированный клиентский и серверный код на десятке языков. HTTP/2 — транспорт с бинарным кадрированием и мультиплексированием: множество параллельных вызовов идёт по одному соединению без блокировки друг другом, заголовки сжимаются, соединение живёт долго. Библиотеки времени исполнения — дедлайны, отмена вызовов, метаданные, перехватчики, встроенная поддержка балансировки.

syntax = "proto3";

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc WatchOrders(OrderRequest) returns (stream OrderUpdate);
}

message GetUserRequest {
  int64 user_id = 1;
}

message User {
  int64 id = 1;
  string name = 2;
  string email = 3;
}

IDL делает контракт строгим и явным: типы полей, их номера и допустимые значения зафиксированы в файле, который хранится в репозитории и версионируется вместе с кодом. Эволюция подчиняется правилам: новые поля добавляются с новыми номерами, старые — резервируются, и старые клиенты продолжают работать с новыми серверами (и наоборот) без «версий API» в REST-смысле. Расхождение контракта и кода ловится на этапе сборки, а не в продакшене; команда потребителя генерирует клиент из чужого .proto-файла — ручное описание не пишется никогда.

Четыре вида вызовов покрывают сценарии от классических запросов до потоков:

Вид вызоваКлиентСерверТиповой сценарий
Unaryодин запросодин ответклассический запрос-ответ (GetUser)
Server streamingодин запроспоток ответовподписка на изменения, выгрузка данных
Client streamingпоток запросоводин ответпакетная загрузка, поток телеметрии
Bidirectionalпоток запросовпоток ответовчат, живая синхронизация состояния

Производительность — традиционная сильная сторона семейства: бинарный формат заметно компактнее и дешевле в обработке, чем текстовый JSON; долгоживущие HTTP/2-соединения убирают накладные расходы на установление связи; мультиплексирование позволяет одному клиенту нагружать сервер множеством параллельных вызовов без пула соединений. Ошибки передаются типизированными статусами (OK, NOT_FOUND, UNAVAILABLE, DEADLINE_EXCEEDED), а дедлайн, назначенный клиентом, распространяется по всей цепочке вызовов — вышестоящий вызов отменяется, когда отменён нижестоящий.

Вокруг gRPC сложилась богатая эксплуатационная инфраструктура. Поскольку вызовы идут поверх HTTP/2, их терминируют и маршрутизируют обычные прокси и балансировщики; сервис-меш добавляет взаимную аутентификацию, повторные попытки и метрики для gRPC-трафика без правок кода сервисов — тот же слой, что описан в интеграционных паттернах для внутреннего обмена.

Где RPC уместен, а где нет. Родная территория gRPC — внутренняя межсервисная коммуникация: восток-запад трафик между сервисами микросервисной системы, где строгий контракт и кодогенерация окупаются мгновенно, где важны задержки и пропускная способность, а браузер никогда не станет потребителем. Там же, где потребители внешние, ограничения выходят на первый план: браузеры говорят с gRPC только через gRPC-web — прослойку с урезанными возможностями; отладка «руками» требует отдельных инструментов (gRPCurl, grpcui) вместо привычного HTTP-клиента; нативного HTTP-кэширования нет по построению; а внешние партнёры ожидают REST и JSON. Публичные API на чистом gRPC — редкость, оправданная только специфичной аудиторией.

GraphQL

GraphQL — язык запросов и среда исполнения, созданные в Facebook (внутренне с 2012, открытая публикация в 2015; с 2018 развивается под крылом нейтральной GraphQL Foundation). Идея: вместо набора адресов или операций система экспонирует один эндпоинт и схему — типизированное описание графа данных; клиент сам перечисляет, какие поля каких объектов ему нужны, и получает ответ ровно в запрошенной форме — ни лишнего, ни недостающего поля.

query {
  user(id: 42) {
    name
    orders(last: 3) {
      total
    }
  }
}

Схема — контракт и документация одновременно. Она описывает типы объектов, их поля, связи между ними и точки входа (Query для чтения, Mutation для изменений, Subscription для подписок):

type User {
  id: ID!
  name: String!
  email: String!
  orders(last: Int = 3): [Order!]!
}

type Order {
  id: ID!
  total: Money!
}

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

Исполнение запроса распределяется по resolver-ам — функциям, отвечающим за каждое поле: резолвер поля user находит пользователя в хранилище, резолвер поля orders — его заказы, причём каждый может ходить в свой источник: базу, соседний сервис, внешний API. Тем самым GraphQL естественно работает как агрегатор: один запрос собирает данные, которые в REST-мире потребовали бы нескольких обращений к нескольким сервисам. Схема типизирована и поддерживает интроспекцию — запрос о самой схеме; из этого растёт экосистема инструментов: интерактивные среды (GraphiQL), генерация типизированных клиентов, валидация запросов до их отправки.

Сильные стороны покупаются сложностью на сервере, и у неё четыре известных лика. Проблема N+1: запрос перечисляет поля, и наивное исполнение вызывает резолвер каждого поля каждого элемента каждого списка — экран из тридцати заказов порождает тридцать обращений к хранилищу. Стандартное лечение — батчинг: резолверы накапливают запросы на один такт исполнения и выполняют их одним пакетным обращением (паттерн DataLoader). Кэширование: классический HTTP-кэш почти неприменим — ответ зависит не только от адреса, но и от тела запроса, поэтому кэш переезжает на клиента, который нормализует граф объектов и хранит его по идентификаторам (Apollo Client и аналоги); серверные же решения — кэширование на уровне полей и закреплённые запросы (persisted queries). Авторизация и контроль сложности: проверки прав приходится накладывать на уровне полей, а произвольная сложность клиентских запросов требует явных ограничений — глубины, числа псевдонимов, оценки стоимости. Наблюдаемость и семантика транспорта: все операции идут в один адрес методом POST, поэтому мониторинг и ограничения трафика строются по именам операций, а не по адресам и методам.

Где GraphQL уместен. Родная ниша — системы с многими разнородными клиентами: мобильное приложение, веб, Smart TV и партнёрские интеграции предъявляют разные требования к форме данных, и схема с клиентскими запросами снимает конфликт потребностей без версионирования каждого эндпоинта. Вторая ниша — агрегация поверх существующих сервисов: GraphQL как слой BFF (подробнее — в интеграционных паттернах) собирает данные нескольких REST/gRPC-сервисов для конкретного экрана. Анти-сценарий — один внутренний клиент с простыми потребностями: схема, резолверы и клиентский кэш станут дорогой инфраструктурой вокруг задачи, которую закрыл бы один REST-эндпоинт. О проектировании самих типов и границ данных, которые схема экспонирует, — в DDD.

Сравнительная таблица

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

ОсьRESTRPC / gRPCGraphQL
Базовая модельРесурсы с адресамиПроцедуры и операцииГраф типизированных данных
ПротоколHTTP/1.1, HTTP/2; семантика методовHTTP/2 (свой протокол поверх него)Обычно HTTP POST к одному эндпоинту
КонтрактСоглашение о методах и кодах; OpenAPI как надстройкаProtobuf IDL, обязателен, с кодогенерациейСхема, обязательна, с интроспекцией
Формат данныхJSON (текстовый)Бинарный ProtobufJSON
Выборка данныхФиксирована сервером; over-/under-fetchingФиксирована набором операцийКлиент задаёт форму ответа
КэшированиеНативное HTTP-кэширование (ETag, Cache-Control, CDN)Не предусмотреноHTTP-кэш неприменим; клиентский нормализованный кэш
СтримингНет встроенного (SSE/WebSocket отдельно)Четыре вида: unary, server-, client-, bidirectionalПодписки (Subscription, обычно WebSocket)
ПроизводительностьТекстовые тела ответов; в HTTP/1.1 — один запрос на соединениеБинарная сериализация, мультиплексирование, дедлайныОверхед исполнения резолверов; риск N+1 без батчинга
Порог входаНизкий: протокол знаком каждомуСредний: IDL, кодогенерация, инструментыВысокий: схема, резолверы, кэш, контроль сложности
Инструменты и отладкаHTTP-клиент, curl, повсеместные проксиgRPCurl, grpcui; нужен .protoGraphiQL, кодогенерация клиентов
Обработка ошибокКоды ответов HTTP и документы ошибокТипизированные статусы (NOT_FOUND, UNAVAILABLE, …)Поле errors в ответе; возможны частичные результаты
Эволюция и версионированиеВерсии в URI или заголовках; ломкие изменения — новая версияНомера полей Protobuf: старые клиенты совместимы с новыми серверамиНовые поля не ломают клиентов; удаление — через объявление устаревшим
НаблюдаемостьПо адресам и методамПо методам и статусамПо именам операций; один эндпоинт требует отдельной аналитики
Типовые сценарииПубличные API, CRUD-доменыВнутренний трафик между сервисами, стримингАгрегация для многих разнородных клиентов, BFF

Из таблицы видно, что стили плохо сравнивать «по общему зачёту»: у каждого своя сильная ось, и она достигается ценой слабых. REST выигрывает там, где важны простота, кэширование и универсальность потребителей; gRPC — где решают задержки, строгий контракт и потоки; GraphQL — где форма данных должна определяться клиентом. Разумная постановка вопроса — не «какой стиль лучше», а «какая ось критична для этой границы системы».

Второй практический вывод: контрактная ось различает стили сильнее всего. REST оставляет контракт неявным (OpenAPI описывает его постфактум), gRPC делает его обязательным артефактом сборки, GraphQL — живым документом с интроспекцией. Отсюда разная цена координации команд: в gRPC-мире контрактные изменения обсуждаются на ревью .proto-файлов, в GraphQL-мире — на ревью схемы, в REST-мире — по публикациям новой версии спецификации.

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

Наконец, за осью «порог входа» стоит организационная цена: REST-интерфейс понятно читается и тестируется почти любым разработчиком, для gRPC нужна дисциплина владения IDL и кодогенерацией в каждом языке парка, для GraphQL — понимание схемы, резолверов и клиентского кэша сразу у серверной и клиентской команд. Инструменты снижают порог, но не отменяют его: обучение и найм — такая же часть цены стиля, как задержки и объём передаваемых данных.

Сводка одной строкой на стиль:

  • REST — выбор по умолчанию для внешних границ и CRUD-доменов: простота, кэширование, универсальность потребителей.
  • RPC/gRPC — выбор по умолчанию для внутреннего трафика между сервисами: контракт, скорость, стриминг.
  • GraphQL — осознанное усложнение ради многих разнородных клиентов: форма данных задаётся на стороне клиента.

Когда что выбирать

CRUD-домены — REST. Если доминирующие операции сводятся к созданию, чтению, изменению и удалению сущностей, грамматика ресурсов ложится на задачу без натяжки: адрес сущности, метод как действие, коды ответов как исходы, HTTP-кэш как бесплатное ускорение чтения. Публичные API почти всегда остаются REST/JSON: потребители безгранично разнообразны, а порог входа должен быть минимальным.

Действия, команды и внутренняя коммуникация — RPC/gRPC. Если API выражает глаголы — перевести деньги, запустить процесс, подтвердить доставку, — а потребители находятся внутри системы, естественен вызов процедуры с типизированным контрактом. gRPC уместен и там, где сценарий требует стриминга: телеметрия, подписки, двунаправленные потоки. Связка «gRPC между сервисами» — рабочий стандарт микросервисных ландшафтов; границы самих сервисов при этом проектируются по своим правилам — см. DDD и гексагональную архитектуру.

Сложные выборки для многих клиентов — GraphQL. Если экраны разных клиентов собирают данные из многих источников и требования к форме ответа расходятся, схема с клиентскими запросами снимает целые классы конфликтов — ценой инфраструктуры на сервере. Правило-ограничитель: не вводить GraphQL ради одного внутреннего клиента — агрегация оправдана, когда клиентов и форм данных действительно много.

Стили различаются не набором «возможностей», а тем, чью работу они упрощают: работу клиента, собирающего данные (GraphQL), работу проектировщика контракта (gRPC) или работу случайного внешнего разработчика (REST). Оценка «с точки зрения технологии» почти всегда заканчивается спором о вкусах; оценка «с точки зрения доминирующего потребителя» — конкретным решением.

Перед решением полезно ответить на несколько контрольных вопросов:

  • Какой тип операций доминирует: чтение сущностей, вызов действий или составные выборки?
  • Кто потребители — внешние партнёры, собственные фронтенды, соседние сервисы; сколько их и насколько они разнородны?
  • Критично ли кэширование промежуточными звеньями: браузерами, обратными прокси, CDN?
  • Нужен ли стриминг данных — в одну сторону или в обе?
  • Какая команда будет владеть серверной стороной и её инструментами?
  • Есть ли у границы внешние потребители, для которых важны документируемость и нулевой порог входа?

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

Доминирующая задачаСтильПочему
CRUD над сущностями, публичный APIRESTРесурсы и методы ложатся на задачу; HTTP-кэш; нулевой порог входа
Действия-команды, внутренний трафик, стримингRPC/gRPCТипизированный контракт, производительность, четыре вида потоков
Сложные выборки, много разнородных клиентовGraphQLКлиент задаёт форму; агрегация без версионирования эндпоинтов
Один внутренний клиент, простые потребностиПростейший из доступныхИнфраструктура GraphQL избыточна

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

Из того же корня растёт правило экономии: пока клиентов мало и все они «свои», любой из трёх стилей справится с задачей, и разумнее выбрать тот, которым команда владеет, — инфраструктура стиля должна догонять реальные потребности, а не опережать их на годы. Наконец, ни один стиль не отменяет дисциплины контракта: осмысленные коды ошибок, явные правила совместимости и документированное поведение нужны и в REST, и в gRPC, и в GraphQL — стиль определяет грамматику границы, но не заменяет проектирования самой границы.

Гибридные подходы

Правило «одна граница — один стиль» удерживает API от смешения грамматик в пределах одного контракта, но не запрещает разные стили на разных границах системы. Для зрелых систем комбинация — норма, а не компромисс:

  • REST с элементами RPC. Действия моделируются «под-ресурсами»: POST /orders/17/cancellation создаёт отмену — сохраняется HTTP-кэшируемость чтений и единообразие, а глагол получает законное место.
  • GraphQL поверх REST-сервисов. Существующие REST API не трогаются; GraphQL-слой агрегирует их для фронтендов — по сути, BFF-шлюз, спроектированный в интеграционных паттернах.
  • gRPC внутри, REST/GraphQL на краю. Сервисы общаются на gRPC ради контракта и скорости; наружу система выставляет REST или GraphQL, преобразование берёт на себя шлюз.
  • Роль API Gateway. Шлюз — место, где стили встречаются: маршрутизация, терминация TLS, аутентификация, ограничение нагрузки и преобразование протоколов (REST снаружи — gRPC внутри) настраиваются в одной точке периметра.

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

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

       Мобильное приложение     Веб-приложение      Партнёрские системы
                │                       │                     │
                ▼                       ▼                     ▼
       ┌─────────────────────────────────────────────────────────────┐
       │                  API Gateway (периметр)                     │
       │     GraphQL (BFF-агрегация)      REST/JSON   REST/JSON      │
       └──────────────────────────────┬──────────────────────────────┘
                                      │  gRPC / Protobuf (внутренний трафик)
                ┌─────────────────────┼─────────────────────┐
                ▼                     ▼                     ▼
         ┌───────────────┐      ┌────────────┐      ┌────────────┐
         │ Пользователи  │      │   Заказы   │      │  Биллинг   │
         └───────────────┘      └────────────┘      └────────────┘

Линию «внутри/снаружи» при этом рисуют по владельцам и темпам, а не по технологиям: пока обе стороны границы под одним владельцем и в одном релизном цикле, им хватает вызова процедуры или функции; как только у сторон появляются разные команды, разные скорости изменений и обязательства обратной совместимости, границе нужен настоящий контракт — и вместе с ним возникает выбор стиля.

Смену стиля на живой границе стоит планировать как постепенную миграцию: новый стиль вводится для новых операций, старые переносятся за несколько релизов, а шлюз на переходный период держит обе формы, пока потребители переходят. Требовать «везде одинаково» не нужно — достаточно, чтобы на каждой границе выбор был осознанным, зафиксированным и не менялся произвольно от сервиса к сервису.

Две оговорки завершают картину. Во-первых, любой синхронный стиль — это контракт на сейчас: обе стороны должны быть доступны одновременно, и там, где нужна развязка во времени, синхронный обмен дополняется асинхронным — событийно-ориентированной архитектурой и очередями сообщений; выбор стиля API и выбор транспорта — независимые решения. Во-вторых, выбор фиксируется в архитектурных документах системы: границы, вдоль которых идут вызовы в каждом стиле, — предмет контейнерной диаграммы C4, а компромиссы всей совокупности решений — предмет System Design и масштабируемости.

Связанные статьи