Проектирование под фронтенд: overfetching и GraphQL

Почему «правильный» REST бывает неудобен фронтенду и какими приёмами это лечат — вплоть до намёка на GraphQL.

Overfetching — клиент получает больше данных, чем нужно. Underfetching — за один запрос данных не хватает, и приходится делать ещё несколько.

Две беды экранов и ресурсов

REST моделирует ресурсы: /users/7, /orders. Но фронтенд рисует экраны, и форма экрана редко совпадает с формой ресурса. Отсюда два хронических неудобства.

Overfetching. Чтобы показать в шапке имя и аватар, фронтенд дёргает GET /users/7 — и получает заодно адрес, телефон, настройки уведомлений, дату регистрации. Девяносто процентов байтов улетают в мусор. На мобильной сети это лишний трафик и медленный рендер.

Underfetching и водопад. Обратная беда. Чтобы показать заказ, нужны сам заказ, его позиции и покупатель. В «чистом» REST это три запроса. Хуже, если они зависят друг от друга: сначала GET /orders/42, из ответа узнали user_id, потом GET /users/7. Получается водопад — запросы выстраиваются в цепочку и складываются их задержки. А список из 20 заказов, где для каждого тянут покупателя, — это классическая проблема N+1 запросов.

  underfetching / водопад:

  GET /orders/42 ──> (узнали user_id=7)
                       │
                       ▼
                 GET /users/7 ──> (узнали данные покупателя)
                                    │
                                    ▼
                              GET /orders/42/items

  суммарная задержка = t1 + t2 + t3   (последовательно!)

Приёмы в REST: выбор полей и разворачивание связей

Большую часть боли снимают, не уходя от REST. Базовые приёмы — управлять формой ответа через query-параметры.

?fields= — отдать только нужные поля

Клиент перечисляет поля, которые ему нужны, и сервер режет лишнее (это называют sparse fieldsets):

GET /users/7?fields=id,name,avatar_url
{ "id": 7, "name": "Анна", "avatar_url": "/img/7.png" }

Overfetching побеждён: вместо двадцати полей пришло три.

?expand= — развернуть связанные ресурсы

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

GET /orders/42?expand=customer,items
{
  "id": 42,
  "total": 1990,
  "customer": { "id": 7, "name": "Анна" },
  "items": [ { "sku": "A1", "qty": 2 } ]
}

Водопад из трёх запросов схлопнулся в один. Так underfetching лечат, не ломая ресурсную модель.

Составные эндпоинты и BFF

Когда экрану нужен сложный «коктейль» из нескольких ресурсов, делают составной эндпоинт под конкретную задачу, например GET /orders/42/summary, который сразу собирает заказ, позиции и покупателя в одном ответе. Доведённая до системы идея — это BFF (Backend For Frontend): отдельный тонкий бэкенд-слой между фронтендом и доменными сервисами. Он принимает «экранные» запросы, сам ходит в нужные API, агрегирует ответы и отдаёт фронтенду ровно ту форму, что нужна экрану. У веба и мобильного приложения могут быть разные BFF, потому что у них разные экраны и сети.

ПриёмЛечитЦена
?fields=overfetchingчуть сложнее сериализация
?expand=underfetching / водопадриск тяжёлых ответов, N+1 на сервере
составной эндпоинтсложный экранэндпоинт «заточен» под один UI
BFFи то, и другоеещё один сервис в эксплуатации

Как работает под капотом: где прячется N+1

Параметр ?expand= и BFF переносят проблему с клиента на сервер, но не уничтожают её. Если сервер на GET /orders?expand=customer для каждого из 50 заказов делает отдельный SELECT покупателя — это тот же N+1, просто внутри сервера. Поэтому реализация expand обязана собирать связанные сущности пачкой: взять все user_id из заказов и сделать один запрос WHERE id IN (...). Хороший expand экономит сетевые round-trip клиента, но требует аккуратной выборки данных на бэкенде, иначе вы просто спрятали тормоза.

Связь с GraphQL

GraphQL — язык запросов к API, где клиент сам описывает, какие поля и связи ему нужны, и получает ровно их одним запросом через единственный эндпоинт.

Заметьте: всё, чего мы добивались приёмами ?fields= и ?expand=, — это «дать клиенту выбирать форму ответа». GraphQL делает ровно это как встроенную идею. Запрос выглядит так (это не REST, а иллюстрация идеи):

query {
  order(id: 42) {
    total
    customer { name }
    items { sku qty }
  }
}

Один POST на /graphql — и клиент получил заказ с покупателем и позициями, ровно нужные поля, без overfetching и без водопада. За это платят сложностью: на сервере нужен слой резолверов (где снова стережёт N+1), кеширование по HTTP-кодам и URL больше не работает «само», а защита от слишком тяжёлых запросов ложится на вас. Это не «замена REST вообще», а инструмент под конкретную боль — много разных экранов с разными потребностями в данных.

Частые ошибки

Считают overfetching мелочью. На мобильной сети лишние килобайты в каждом ответе — это заметная задержка и расход батареи; экономия полей окупается.

Лечат водопад параллельными запросами «в лоб». Иногда это работает, но N+1 на список так не убрать — нужен expand, пакетная выборка или составной эндпоинт.

Делают эндпоинт «на все случаи». Один ?expand= со всеми связями сразу возвращает гигантский ответ — снова overfetching, только хуже. Разворачивайте лишь то, что нужно экрану.

Берут GraphQL «потому что модно». Для одного простого фронтенда REST с ?fields=/?expand= проще в эксплуатации; GraphQL оправдан, когда экранов и клиентов много и они разные.

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

Итоги

  • Overfetching (лишние поля) и underfetching (водопад, N+1) рождаются из разрыва «ресурсы против экранов».
  • В REST их лечат приёмами ?fields=, ?expand=, составными эндпоинтами и слоем BFF.
  • expand/BFF переносят N+1 на сервер — там нужна пакетная выборка IN (...), а не запрос на сущность.
  • GraphQL доводит идею «клиент выбирает поля» до предела; это инструмент под много разных клиентов, а не безусловная замена REST.
Проверьте себя
1. Что такое underfetching?
AКлиент получает больше полей, чем нужно
BЗа один запрос данных не хватает, и приходится делать ещё несколько запросов
CСервер отдаёт ответ медленнее лимита
DКлиент кеширует ответы слишком агрессивно
2. Какой приём в REST помогает встроить связанный ресурс (например, покупателя) прямо в ответ заказа?
A?fields=
B?expand=
CIdempotency-Key
DHATEOAS
3. В чём роль слоя BFF (Backend For Frontend)?
AХранит пароли пользователей
BАгрегирует данные из нескольких API в форму, удобную конкретному экрану/клиенту
CЗаменяет базу данных
DПодписывает webhook-события
4. Какую идею REST-приёмов ?fields= и ?expand= GraphQL делает встроенной?
AАвтоматическое кеширование по URL
BКлиент сам выбирает, какие поля и связи получить, одним запросом
CОбязательную HMAC-подпись ответов
DГипермедийные ссылки в ответе