Когда пользователь открывает список заказов, он хочет видеть только свои, только за прошлый месяц, только отменённые — и желательно отсортированные по дате. Всё это передаётся через 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-методы и статусы — когда какой метод использовать.