June 16

Как правильно распределять данные в REST API

Когда мы проектируем REST API, один из первых практических вопросов звучит так: какие данные куда класть в запросе?

У нас есть путь, query‑параметры, заголовки и тело — и каждый из этих элементов решает свою задачу.

В этой статье разберёмся на конкретном кейсе: витрина каталога товаров в мобильном приложении.


Кейс: витрина каталога и «глупый» фронт

Представим, что у нас есть фронт: iOS, Android и мобильный веб.
Фронт максимально "глупый": он просто ходит в мидл‑сервисы и хочет получить:

  • витрину с каталогом товаров
  • категории товаров
  • корзину пользователя

При этом, вместе с запросом фронт передаёт:

  • авторизационный токен
  • информацию о приложении (версия, тип устройства, операционная система)

Задача: понять, в какую часть HTTP‑запроса какие данные класть, чтобы запросы были логичными, безопасными и удобными для поддержки.

Четыре места для данных в HTTP‑запросе

В типичном REST API у нас есть четыре основных "слота", куда можно положить данные:

  1. Путь (Path) — часть URL, которая описывает, к какому ресурсу мы обращаемся.
  2. Параметры запроса (Query) — строка после ? в URL, используется для фильтров, сортировок и прочих модификаторов выборки.
  3. Заголовки (Headers) — метаданные запроса: авторизация, формат данных, информация о клиенте.
  4. Тело (Body) — основное содержимое: то, что мы создаём, изменяем или отправляем как сложный запрос.

Дальше разложим наш кейс по этим четырём местам.

Авторизационный токен: заголовок Authorization

Bearer‑токен (или любой другой токен авторизации) логичнее всего передавать в заголовке Authorization.
Это де‑факто стандарт для REST API и рекомендованный способ передачи авторизационных данных.

GET /api/v1/products
Authorization: Bearer eyJhbGciOiJIUz...

Почему именно заголовок:

  • токен не попадает в URL, а значит — не засветится в истории браузера или прокси
  • многие инструменты и библиотеки по умолчанию ожидают токен именно в Authorization
  • это согласуется с практиками OAuth 2.0 и большинством публичных API

Метаданные клиента: тип приложения, версия, ОС

Информация о том кто обращается, мобилка, веб. Это метаданные: они описывают не ресурс, а того, кто делает запрос.
Примеры:

  • тип приложения (mobile/web)
  • версия приложения
  • операционная система и её версия

Эти данные нужны серверу, чтобы:

  • принимать решения по фичефлагам (например, отключать функциональность для старых версий)
  • логировать и анализировать трафик по типам клиентов
  • внедрять ограничения: «для версии ниже 1.5 не отдаём новый тип карточек товара»

Так как эти данные нужны при любом запросе, а не только при конкретных методах, логично передавать их в заголовках.

Пример:

X-App-Type: ios
X-App-Version: 1.2.3
X-OS-Name: iOS
X-OS-Version: 16.4

Исторически было принято использовать префикс X- для кастомных заголовков, но сейчас его применение считается устаревшей практикой.
Можно использовать более нейтральные варианты, например: App-Type, App-Version, Client-OS-Name.

Ключевой момент: не класть эти метаданные в тело, потому что:

  • у GET‑запросов тела обычно нет, а метаданные нам нужны и там
  • нам важна предсказуемость: любой запрос несёт одни и те же заголовки клиента
  • прокси и кэширующие слои чаще учитывают заголовки, чем тело, для принятия решений

Фильтры и сортировка: параметры запроса (query)

Когда речь идёт о фильтрации, поиске или сортировке, первый кандидат — query‑параметры.
Они позволяют явно описать, какую выборку мы хотим получить.

Пример запроса витрины:

GET /api/v1/products?sort=price&category=electronics&page=1&limit=20

Здесь:

  • sort=price — сортировка по цене
  • category=electronics — фильтрация по категории
  • page и limit — параметры пагинации

Рекомендации по фильтрам через query:

  • использовать понятные названия (price, category, brand, sort)
  • делать параметры консистентными: например, page/limit везде, а не page/perPage в одном методе и offset/count в другом.
  • для сложных фильтров применять единый формат (например, filter[field]=value или filter=field:op:value)

Когда фильтры уходят в тело запроса

Бывает, что фильтрация становится настолько сложной, что query‑строка становится:

  • слишком длинной
  • с трудом читаемой
  • плохо валидируемой и документируемой

В таких случаях разумно перенести фильтры в тело запроса и использовать POST для получения данных по сложному запросу.

POST /api/v1/products/search
Content-Type: application/json

{
  "sort": { "field": "price", "direction": "asc" },
  "filters": {
    "category": ["electronics", "appliances"],
    "price": { "gte": 1000, "lte": 5000 },
    "inStock": true
  },
  "page": 1,
  "limit": 20
}

Такой подход встречается, когда нужны:

  • сложные комбинированные фильтры
  • вложенные структуры
  • массивы значений и сложные условия

Правило, которое удобно держать в голове:
простые фильтры — в query, сложные и структурированные — в теле запроса.

И финальное, все данные, которые мы получаем о товарах мы забираем из тела запроса.