Опирается на правила:
R-QRY-1..9иR-QRY-X1..X5из REST API Style Guide → раздел Query-параметры и пагинация.
Важно знать
- camelCase для имён query-параметров (
customerId,dateFrom).- Фильтрация — имя поля как параметр (
?status=CONFIRMED).- Диапазоны через суффиксы
From/To(?dateFrom=...&dateTo=...).- Offset-based —
page(1-based!) +size. Для UI с номерами страниц.- Cursor-based —
cursor(непрозрачный токен) +size. Для feed-style.- Сортировка —
sort=поле,direction(sort=createdAt,desc).- Множественные значения — повтор параметра (
?status=A&status=B).- Сложные запросы —
POST /resources/searchс JSON body.
Query — основной механизм фильтрации и пагинации. UCP формулирует чёткие правила: одна конвенция per use-case (offset vs cursor), параметры в camelCase, никаких comma-separated массивов или бизнес-команд в query.
Имена параметров
R-QRY-1: camelCase.
GET /orders?customerId=123&dateFrom=2026-01-01 ✓
GET /orders?customer_id=123 ✗ — snake_case
GET /orders?CustomerID=123 ✗ — PascalCase
Фильтрация и диапазоны
R-QRY-2..3:
GET /orders?status=CONFIRMED ✓ фильтр по полю
GET /orders?customerId=550e8400-... ✓
GET /orders?dateFrom=2026-01-01&dateTo=2026-12-31 ✓ диапазон
GET /orders?amountFrom=100&amountTo=500 ✓
From/To — [from, to] интервал inclusive. Это конвенция, не RFC, но distinct и понятна.
Offset-based пагинация
R-QRY-4: page (1-based!) + size.
GET /orders?page=1&size=20 ← первая страница
GET /orders?page=3&size=50 ← третья страница
Контракт API — 1-based. Первая страница = 1, не 0.
# application.yml
spring:
data:
web:
pageable:
one-indexed-parameters: true
Ручная конвертация page - 1 в коде запрещена — теряется единая точка истины.
Response:
{
"content": [
{ "orderId": "...", "status": "CREATED" }
],
"page": 1,
"size": 20,
"totalElements": 243,
"totalPages": 13
}
Когда: произвольный переход на любую страницу, UI с номерами, нужно общее количество, данные относительно статичны.
Ограничения: при вставке/удалении между запросами элементы могут дублироваться или пропускаться; OFFSET в SQL деградирует на больших значениях.
Cursor-based пагинация
R-QRY-5: cursor (opaque) + size.
GET /orders?size=20 ← первая страница
GET /orders?size=20&cursor=eyJpZCI6MTAwfQ== ← следующая
Cursor — непрозрачный токен из предыдущего ответа. Клиент не парсит, не конструирует.
Response:
{
"content": [...],
"size": 20,
"nextCursor": "eyJpZCI6MTIwfQ==",
"prevCursor": "eyJpZCI6MTAwfQ==",
"hasNext": true,
"hasPrev": true
}
Когда: часто меняющиеся данные (feeds, messages, notifications); большие объёмы; infinite scroll; real-time потоки.
Ограничения: нет произвольного перехода на страницу N; нельзя узнать totalElements без отдельного запроса.
Server-side cursor — обычно Base64-encoded (lastId, lastCreatedAt). Клиенту не видны, чтобы можно было изменить схему без breaking change.
Сортировка
R-QRY-6: sort=field,direction.
GET /orders?sort=createdAt,desc
GET /orders?sort=totalAmount,asc&sort=createdAt,desc
Множественная сортировка через повтор sort. Допустима, но должна быть обоснована (риск проблем с составными индексами в БД — см. PG explain).
Полнотекстовый поиск
R-QRY-7: q.
GET /orders?q=term
GET /products?q=клавиатура
Один параметр для full-text. Сложные search-запросы — через POST /search.
Множественные значения
R-QRY-8: повтор параметра.
GET /orders?status=CREATED&status=CONFIRMED ✓
GET /orders?status=CREATED,CONFIRMED ✗ — comma-separated
Стандарт OpenAPI:
parameters:
- name: status
in: query
schema:
type: array
items:
type: string
style: form
explode: true
style: form, explode: true — повтор. Это default behaviour Spring и большинства фреймворков.
R-QRY-X3: comma-separated ?status=A,B — запрещено:
- Требует ручного парсинга на backend.
- Ломается если значение содержит запятую.
- Не default в OpenAPI.
POST /resources/search
R-QRY-9: для сложных запросов.
GET имеет практические ограничения:
- Длина URL — 2000-8000 символов (зависит от proxy).
- Невозможность передать вложенные структуры в query.
Когда поисковый запрос не укладывается:
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
}
Критерии перехода с GET на POST:
| Плоские поля (status, date) | GET достаточно | | Вложенные объекты | POST | | Массив 10+ значений в одном фильтре | POST | | Спецсимволы или свободный текст | POST | | AND/OR-комбинации | POST | | Запрос нужно сохранять/переиспользовать | POST |
Правила POST-поиска:
- URL:
/resources/search(не/query, не/find). - Метод:
POST— GET с body формально не запрещён RFC, но игнорируется прокси и кешами. - Код ответа:
200 OK(ресурс не создаётся). - Формат ответа — тот же, что у
GET /resources(пагинированный список). - Идемпотентность: при кешировании —
Idempotency-Key(см. Заголовки).
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
?customer_id= snake_case | R-QRY-X1 | camelCase |
page=0 (0-based в публичном контракте) | R-QRY-X2 | page=1 |
?status=A,B comma-separated | R-QRY-X3 | ?status=A&status=B |
?action=cancel бизнес-логика в query | R-QRY-X4 | POST /orders/{id}/cancel |
| Клиент парсит cursor | R-QRY-X5 | opaque token |
?dateFrom=2026-01-01T00:00:00Z без dateTo | R-QRY-3 | оба или один (from only) |
| GET search с длинным URL | R-QRY-9 | POST /search |
| Cursor с PII в payload | R-QRY-5 | Base64-encoded internal IDs |
Куда дальше
- REST API → Query-параметры (нормативно) — формулировки.
- URL и ресурсы — формат пути.
- JSON и формат ответов — пагинированный response.
- Заголовки и трассировка — Idempotency-Key для POST search.
- Batch, async, локализация — async-поиск.
- PG explain — индексы под сортировку.