9. Query-параметры

9.1 Обязательно

  • R-QRY-1. Имена параметров — camelCase.

    GET /api/v1/orders?customerId=123&dateFrom=2025-01-01    -- правильно
    
  • R-QRY-2. Фильтрация по полю — имя поля как параметр.

    GET /api/v1/orders?status=CONFIRMED
    GET /api/v1/orders?customerId=550e8400-e29b-41d4-a716-446655440000
    
  • R-QRY-3. Диапазоны — суффиксы From / To.

    GET /api/v1/orders?dateFrom=2025-01-01&dateTo=2025-12-31
    GET /api/v1/orders?amountFrom=100&amountTo=500
    
  • R-QRY-4. Offset-based пагинация: параметры page (1-based) и size.

    GET /api/v1/orders?page=1&size=20
    GET /api/v1/orders?page=3&size=50
    
    • page — номер страницы, 1-based: первая страница = 1.
    • size — количество элементов на странице. Значение по умолчанию задаётся сервером (например, 20).

    Реализация: контракт API — 1-based; в Spring Data конфигурируется spring.data.web.pageable.one-indexed-parameters=true. Ручная конвертация page-1 в коде запрещена — теряется единая точка истины.

    Пример ответа:

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

    Когда использовать: произвольный переход на любую страницу; нужно знать общее количество (totalElements); данные относительно статичны; UI с номерами страниц.

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

  • R-QRY-5. Cursor-based пагинация: параметры cursor (непрозрачный токен) и size.

    GET /api/v1/orders?size=20                              -- первая страница
    GET /api/v1/orders?size=20&cursor=eyJpZCI6MTAwfQ==      -- следующая страница
    
    • cursor — непрозрачный токен из предыдущего ответа. Клиент НЕ парсит и НЕ конструирует его.
    • size — количество элементов.

    Пример ответа:

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

    Когда использовать: часто меняющиеся данные (ленты, сообщения, уведомления); большие объёмы; infinite scroll; real-time потоки.

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

  • R-QRY-6. Сортировка — параметр sort, формат имяПоля,направление.

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

    Множественная сортировка через повтор параметра допустима, но должна быть обоснована (риск проблем с составными индексами).

  • R-QRY-7. Полнотекстовый поиск — параметр q.

    GET /api/v1/orders?q=термин
    GET /api/v1/products?q=клавиатура
    
  • R-QRY-8. Множественные значения — повтор параметра.

    GET /api/v1/orders?status=CREATED&status=CONFIRMED     -- правильно
    

    Это стандартный способ передачи массивов в query string. Поддерживается из коробки большинством фреймворков и корректно описывается в OpenAPI через style: form, explode: true.

  • R-QRY-9. Сложные поисковые запросы — POST /resources/search с телом JSON.

    GET имеет практические ограничения: длина URL (~2000–8000 символов), невозможность передать вложенные структуры в query. Когда поисковый запрос не укладывается — POST /resources/search:

    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
    • Массив 10+ значений в одном фильтре → POST
    • Спецсимволы или свободный текст → POST (GET ломает читаемость из-за кодирования)
    • AND/OR-комбинации → POST
    • Запрос нужно сохранять/переиспользовать → POST

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

    1. URL: /resources/search (не /query, не /find, не /search/resources).
    2. Метод: POST — GET с телом формально не запрещён RFC, но на практике тело игнорируется прокси, кешами и многими клиентами.
    3. Код ответа: 200 OK (ресурс не создаётся).
    4. Формат ответа — тот же, что у GET /resources (пагинированный список).
    5. Идемпотентность: POST-поиск идемпотентен по факту, но HTTP этого не гарантирует — при необходимости кеширования добавь заголовок Idempotency-Key (см. R-HDR-3 в Заголовки).

9.2 Запрещено

  • R-QRY-X1. snake_case или PascalCase в именах параметров.
    ?customer_id=123     -- неправильно
    ?CustomerID=123      -- неправильно
    
  • R-QRY-X2. page=0 или 0-based нумерация в публичном контракте.
  • R-QRY-X3. Comma-separated значения для массивов.
    ?status=CREATED,CONFIRMED            -- неправильно
    

    Причины: требует ручного парсинга на бэкенде; ломается, если значение содержит запятую; не соответствует поведению по умолчанию OpenAPI (explode: true в style: form).

  • R-QRY-X4. Бизнес-логика в query-параметре вместо action-эндпоинта.
    /orders?action=cancel    -- неправильно
    POST /orders/{id}/cancel -- правильно
    
  • R-QRY-X5. Парсинг или конструирование cursor на стороне клиента.