Проектирование под фронтенд: 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.