REST API Style Guide: Query-параметры и пагинация
Query-параметры REST API: camelCase, фильтрация, offset и cursor пагинация.
REST API пагинация query параметры: 9. Query-параметры
9.1. camelCase
REST API пагинация query параметры — gET /api/v1/orders?customerId=123&dateFrom=2025-01-01 -- правильно
GET /api/v1/orders?customer_id=123&date_from=2025-01-01 -- неправильно
GET /api/v1/orders?CustomerID=123 -- неправильно
9.2. Фильтрация -- имя поля
GET /api/v1/orders?status=CONFIRMED
GET /api/v1/orders?customerId=550e8400-e29b-41d4-a716-446655440000
9.3. Диапазоны -- суффикс From / To
GET /api/v1/orders?dateFrom=2025-01-01&dateTo=2025-12-31
GET /api/v1/orders?amountFrom=100&amountTo=500
9.4. Пагинация
Два подхода: offset-based и cursor-based. Выбор зависит от характера данных и требований.
Offset-based (страничная)
Классический подход: клиент указывает номер страницы и размер.
GET /api/v1/orders?page=1&size=20
GET /api/v1/orders?page=3&size=50
page-- номер страницы, 1-based (первая страница = 1)size-- количество элементов на странице, значение по умолчанию задается сервером (например, 20)
Пример ответа:
{
"content": [
{ "orderId": "...", "status": "CREATED" },
{ "orderId": "...", "status": "CONFIRMED" }
],
"page": 1,
"size": 20,
"totalElements": 243,
"totalPages": 13
}
Когда использовать:
- Нужен произвольный переход на любую страницу (страница 1, потом сразу страница 10)
- Нужно знать общее количество (
totalElements,totalPages) - Данные относительно статичны (справочники, каталоги)
- UI с номерами страниц
Ограничения:
- При вставке/удалении записей между запросами элементы могут дублироваться или пропускаться
OFFSETв SQL замедляется на больших смещениях (БД сканирует и отбрасывает строки)
Cursor-based (курсорная)
Клиент получает непрозрачный курсор (токен) для запроса следующей/предыдущей порции данных.
GET /api/v1/orders?size=20 -- первая страница
GET /api/v1/orders?size=20&cursor=eyJpZCI6MTAwfQ== -- следующая страница
cursor-- непрозрачный токен, полученный из предыдущего ответа. Клиент не должен его парсить или конструироватьsize-- количество элементов на странице
Пример ответа:
{
"content": [
{ "orderId": "...", "status": "CREATED" },
{ "orderId": "...", "status": "CONFIRMED" }
],
"size": 20,
"nextCursor": "eyJpZCI6MTIwfQ==",
"prevCursor": "eyJpZCI6MTAwfQ==",
"hasNext": true,
"hasPrev": true
}
Когда использовать:
- Данные часто меняются (лента событий, сообщения, уведомления)
- Большие объемы данных, где offset деградирует по производительности
- Бесконечная прокрутка (infinite scroll)
- Real-time потоки данных
Ограничения:
- Нет произвольного перехода на страницу N
- Нельзя узнать общее количество без отдельного запроса
- Сложнее реализовать на бэкенде
Сравнение
- Offset -- простая реализация, произвольный доступ к страницам, но деградация на больших данных и нестабильность при изменениях
- Cursor -- стабильная выборка, высокая производительность на больших данных, но только последовательная навигация
9.5. Сортировка -- sort
GET /api/v1/orders?sort=createdAt,desc
GET /api/v1/orders?sort=totalAmount,asc&sort=createdAt,desc
Формат: имяПоля,направление. Рекомендуется сортировка по одному полю -- множественная сортировка может приводить к проблемам с производительностью (составные индексы). Сортировка по нескольким полям допустима через повторение параметра, но должна быть обоснована.
9.6. Полнотекстовый поиск -- q
Для полнотекстового поиска по коллекции используется параметр q:
GET /api/v1/orders?q=термин
GET /api/v1/products?q=клавиатура
9.7. Множественные значения -- повтор параметра
GET /api/v1/orders?status=CREATED&status=CONFIRMED -- правильно
GET /api/v1/orders?status=CREATED,CONFIRMED -- неправильно
Повтор параметра -- стандартный способ передачи массивов в query string. Поддерживается из коробки большинством фреймворков и корректно описывается в OpenAPI через style: form, explode: true.
9.8. Сложные поисковые запросы -- POST вместо GET
GET имеет практические ограничения: длина URL (~2000–8000 символов в зависимости от браузера/сервера), невозможность передать вложенные структуры в query-параметрах. Когда поисковый запрос не укладывается в GET -- используйте POST с суффиксом /search:
GET /api/v1/orders?status=CONFIRMED&dateFrom=2025-01-01 -- простой поиск
POST /api/v1/orders/search -- сложный поиск
Тело POST-запроса содержит критерии поиска:
POST /api/v1/orders/search
Content-Type: application/json
{
"statuses": ["CONFIRMED", "PAID", "SHIPPED"],
"dateRange": {
"from": "2025-01-01",
"to": "2025-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 избыточен
- Вложенные объекты в фильтрах -- GET не подходит, используйте POST
- Массив из 10+ значений в одном фильтре -- GET неудобен, используйте POST
- Фильтр содержит спецсимволы или свободный текст -- GET ломает читаемость из-за кодирования, используйте POST
- Комбинация AND/OR условий -- GET не подходит, используйте POST
- Запрос нужно сохранять/переиспользовать -- POST (тело = JSON, легко хранить)
Правила для POST-поиска:
- URL:
/resources/search-- не/resources/query, не/resources/find, не/search/resources - Метод: POST -- потому что GET с телом формально не запрещен RFC, но на практике тело игнорируется прокси, кешами и многими клиентами
- Код ответа:
200 OK-- ресурс не создается, поэтому не 201 - Ответ -- тот же формат, что и у
GET /resources(пагинированный список) - Идемпотентность: POST-поиск идемпотентен по факту (повторный вызов с тем же телом дает тот же результат), но HTTP этого не гарантирует -- при необходимости кеширования добавьте заголовок
Idempotency-Key