← назад к разделу

Когда пользователь открывает список заказов, он хочет видеть только свои, только за прошлый месяц, только отменённые — и желательно отсортированные по дате. Всё это передаётся через query-параметры: часть URL после знака ?.

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

Как называть параметры

Имена query-параметров пишут в camelCase — так же, как поля в JSON.

GET /orders?customerId=123&dateFrom=2026-01-01     ✓
GET /orders?customer_id=123                        ✗ — snake_case
GET /orders?CustomerID=123                         ✗ — PascalCase

Единая конвенция по всему API избавляет клиентов от путаницы: не нужно помнить, где подчёркивание, а где нет.

Фильтрация

Самый простой способ фильтровать — передать имя поля и значение напрямую:

GET /orders?status=CONFIRMED
GET /orders?customerId=550e8400-e29b-41d4-a716-446655440000

Для диапазонов добавляют суффиксы From и To:

GET /orders?dateFrom=2026-01-01&dateTo=2026-12-31
GET /orders?amountFrom=100&amountTo=500

Интервал включает оба края: от dateFrom включительно до dateTo включительно. Можно передать только одну границу — например, dateFrom без dateTo означает «с этой даты и далее».

Два вида пагинации

Когда в базе тысячи записей, отдавать их все за один запрос нельзя. Нужна постраничная загрузка. Есть два подхода, и у каждого своя область применения.

Offset-пагинация — для классического UI со страницами

Клиент говорит: «дай мне страницу номер 3, по 20 штук». Сервер пропускает первые 40 и возвращает следующие 20.

GET /orders?page=1&size=20     ← первая страница
GET /orders?page=3&size=50     ← третья страница

Важная деталь: первая страница — это page=1, а не page=0. Нулевая нумерация страниц внутри кода — это детали реализации, которые не должны протекать в публичный контракт.

В Spring Data для этого есть готовая настройка:

spring:
  data:
    web:
      pageable:
        one-indexed-parameters: true

Ответ сервера содержит сами данные и информацию о постраничности:

{
  "content": [
    { "orderId": "...", "status": "CREATED" }
  ],
  "page": 1,
  "size": 20,
  "totalElements": 243,
  "totalPages": 13
}

totalElements и totalPages позволяют UI нарисовать кнопки «1 2 3 … 13».

Когда использовать: нужны номера страниц в интерфейсе, пользователь хочет перепрыгнуть на страницу 7, данные меняются редко.

Ограничения: при активной вставке/удалении записей страницы «плывут» — элемент может появиться дважды или пропасть. На очень больших OFFSET в SQL запрос замедляется.

Cursor-пагинация — для лент и бесконечной прокрутки

Вместо номера страницы клиент получает непрозрачный токен (cursor) и передаёт его в следующем запросе: «дай мне 20 записей после этой точки».

GET /orders?size=20                                ← первая страница
GET /orders?size=20&cursor=eyJpZCI6MTAwfQ==        ← следующая

Клиент не знает, что внутри cursor, и не должен знать — это Base64-строка, которую сервер читает сам. Конструировать cursor самостоятельно не нужно: берёте значение nextCursor из ответа и подставляете в следующий запрос.

{
  "content": [...],
  "size": 20,
  "nextCursor": "eyJpZCI6MTIwfQ==",
  "prevCursor": "eyJpZCI6MTAwfQ==",
  "hasNext": true,
  "hasPrev": true
}

Когда использовать: данные часто меняются (лента сообщений, уведомления), нужна бесконечная прокрутка, большие объёмы данных.

Ограничения: нельзя перейти сразу на страницу 7 и нельзя узнать общее количество записей без отдельного запроса.

Сортировка

Параметр sort принимает имя поля и направление через запятую:

GET /orders?sort=createdAt,desc
GET /orders?sort=totalAmount,asc

Если нужна многоуровневая сортировка, параметр повторяют:

GET /orders?sort=totalAmount,asc&sort=createdAt,desc

Многоуровневую сортировку стоит применять осторожно: составные индексы в базе данных должны соответствовать порядку полей.

Полнотекстовый поиск

Для свободного текстового поиска используют параметр q:

GET /products?q=клавиатура
GET /orders?q=Иванов

Один параметр, никакой магии. Если поиск сложный — смотрите раздел ниже.

Несколько значений одного фильтра

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

GET /orders?status=CREATED&status=CONFIRMED&status=PAID     ✓
GET /orders?status=CREATED,CONFIRMED,PAID                   ✗

Передавать значения через запятую в одном параметре — частая ошибка. Это ломается, если значение само содержит запятую, и требует ручного разбора на сервере. Повтор параметра — стандартное поведение, которое Spring и большинство фреймворков поддерживают из коробки.

В OpenAPI это описывается так:

parameters:
  - name: status
    in: query
    schema:
      type: array
      items:
        type: string
    style: form
    explode: true

Когда GET не хватает: POST /search

У GET-запроса есть физические ограничения. URL не может быть бесконечно длинным — прокси-серверы обычно режут его на 2000–8000 символах. В query-строке нельзя передать вложенные объекты.

Когда запрос слишком сложный для URL, используют POST /resources/search с JSON-телом:

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

{
  "statuses": ["CONFIRMED", "PAID", "SHIPPED"],
  "dateRange": { "from": "2026-01-01", "to": "2026-12-31" },
  "customer": { "regionIds": [1, 5, 12], "segment": "VIP" },
  "totalAmount": { "from": 1000, "to": 50000 },
  "sort": [
    { "field": "createdAt", "direction": "DESC" },
    { "field": "totalAmount", "direction": "ASC" }
  ],
  "page": 1,
  "size": 20
}

Когда переходить на POST:

  • нужны вложенные объекты в фильтре;
  • массив из 10 и более значений;
  • комбинации AND/OR;
  • запрос нужно сохранять и переиспользовать.

Правила для POST-поиска:

  • URL: /resources/search — не /query, не /find.
  • Код ответа: 200 OK, ресурс не создаётся.
  • Формат ответа такой же, как у GET /resources — тот же пагинированный список.

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

page=0 в публичном API. Нулевая страница — это деталь внутренней реализации (Java-индексация с нуля). Клиенты ожидают, что первая страница = 1.

Значения через запятую. ?status=CREATED,CONFIRMED выглядит компактно, но ломается при значениях с запятой и не соответствует стандарту. Повторяйте параметр.

Бизнес-действие в query. ?action=cancel — это не фильтр, а команда. Команды идут через отдельный endpoint: POST /orders/{id}/cancel.

Клиент разбирает cursor. Если клиент декодирует Base64 и читает поля cursor — это нарушение контракта. Формат cursor может измениться в любой момент; клиент обязан считать его непрозрачной строкой.

Коротко

  • Имена параметров — camelCase: customerId, dateFrom, не customer_id.
  • Фильтрация: имя поля = параметр (?status=CONFIRMED). Диапазоны: From/To суффиксы.
  • Offset-пагинация: page (от 1) + size. Возвращает totalElements. Для UI со страницами.
  • Cursor-пагинация: cursor (непрозрачный токен) + size. Для лент и бесконечной прокрутки.
  • Сортировка: sort=поле,direction; повторяется для многоуровневой.
  • Несколько значений — повтор параметра: ?status=A&status=B, не ?status=A,B.
  • Сложный поиск — POST /resources/search с JSON-телом, ответ 200 OK.

Что почитать дальше

  • URL и структура ресурсов — как строить пути.
  • JSON и формат ответов — структура пагинированного ответа.
  • HTTP-методы и статусы — когда какой метод использовать.