Справочник «Стандартизация HTTP API» · модуль 2 · правила HTTP
Глава 2.4
Параметры,
тело, форматы
Адрес и метод сказали, что делать. Остальное приходит параметрами и телом: фильтр, размер страницы, формат данных, имена полей. В этой главе разберём, где чему место, как устроена пагинация и почему стиль имён в JSON — часть контракта, а не вкус разработчика.
- Категория
- Правила HTTP
- Уровень
- Базовый
- Связанные главы
- 1.3 Ресурсы · 2.3 Коды ответов · 3.2 Разделы OpenAPI · 6.2 Линтер
- Зачем это знать
- Чтобы клиент не выкачивал коллекцию целиком и не гадал, как называются поля
§1Четыре места параметра
Значение может приехать в запросе четырьмя путями, и выбор пути — часть контракта. В описании OpenAPI у параметра есть поле in, которое прямо называет место.
/tickets/{ticket_id}. Всегда обязателен.идентификаторstatus, limit, offset. Обычно необязателен и имеет значение по умолчанию.фильтрContent-Type, If-Match, Idempotency-Key.условияСмешение ролей ломает смысл запроса. Идентификатор в query (/tickets?id=…) превращает обращение к объекту в фильтрацию коллекции: вместо 404 клиент получит пустой список. Фильтр в пути (/tickets/open) выглядит как адрес объекта с идентификатором open — и ломается, как только у обращения появится такой идентификатор.
§2Пагинация: коллекция не бесконечна
Учебная база содержит три обращения, и их можно вернуть одним массивом. Но контракт пишется не под текущий объём данных: когда обращений станет двести тысяч, запрос GET /tickets без ограничения попробует вернуть их все.
Поэтому правило: коллекции, размер которых не ограничен, MUST поддерживать постраничный вывод. В учебном сервисе он устроен параметрами limit и offset.
GET /api/v1/tickets?limit=2&offset=0
HTTP/1.1 200
content-type: application/json
{
"items": [ { … }, { … } ],
"total": 3,
"limit": 2,
"offset": 0
}offset=20.≥ 0Запрос с limit=500 не «подстроится» под максимум молча — он получит 422 и объяснение, какое значение допустимо. Если бы сервис молча заменил значение на максимальное, клиент счёл бы, что получил всю выборку, и часть данных была бы потеряна.
Крупные API часто используют курсорную пагинацию: вместо offset клиент передаёт метку последней полученной записи. Она устойчивее, когда данные добавляются во время листания: при offset новая запись сдвигает всю выборку, и объект может встретиться дважды. Для учебного сервиса выбран offset — он проще и виден целиком.
§3Фильтрация и сортировка
Фильтр — это query-параметр с понятным именем и объявленным набором значений. В учебном сервисе фильтр один:
GET /api/v1/tickets?status=open
Значение status ограничено перечислением open, in_progress, closed. Опечатка вернёт 422, а не пустой список: клиент узнает, что ошибся, а не решит, что обращений нет.
| Задача | Как выражают | На что обратить внимание |
|---|---|---|
| Фильтр по значению | ?status=open | перечислить допустимые значения в описании |
| Фильтр по диапазону | ?created_from=2026-09-01 | единый формат даты — RFC 3339 |
| Сортировка | ?sort=created_at, ?sort=-created_at | договориться о знаке для убывания и зафиксировать в правилах |
| Поиск по тексту | ?q=vpn | описать, по каким полям идёт поиск |
Общее требование: любой параметр, влияющий на выборку, должен быть описан в контракте. Параметр, не указанный в описании, известен только авторам сервиса, и остальные клиенты работают с неполным контрактом.
§4Media type: как читать тело
Тело запроса и ответа — набор байтов. Что это за байты, сообщает заголовок Content-Type с указанием media type.
| Media type | Где встречается | Что означает |
|---|---|---|
application/json | запросы и успешные ответы | обычный документ JSON |
application/problem+json | ответы 4xx и 5xx | описание ошибки по RFC 9457 — разбор в главе 4.1 |
application/x-www-form-urlencoded | формы браузера | пары «поле=значение», не JSON |
multipart/form-data | загрузка файлов | несколько частей в одном теле |
Суффикс +json у application/problem+json говорит: формат основан на JSON, но имеет собственную структуру. Клиент, умеющий разбирать JSON, прочитает тело; клиент, знающий про Problem Details, поймёт смысл полей.
Отправив JSON без заголовка, вы получите ошибку, которая выглядит как «сервер не понял запрос». Сервер действительно не понял: он не обязан угадывать формат по первому символу.
§5Стиль JSON
Имена полей — часть контракта, и менять их потом нельзя. Поэтому стиль выбирают один раз и применяют ко всем ресурсам сразу.
created_at,ticket_id,in_progress;- совпадает со стилем Python;
- проверяется правилом линтера.
createdAt,ticketId;- привычен для клиентов на JavaScript;
- тоже допустим — если применяется везде.
Ошибка не в выборе, а в смешении: created_at в одном ответе и updatedAt в соседнем заставляют клиента помнить исключения. Поэтому правило записывают в style guide и проверяют автоматически.
То же касается времени. В учебном сервисе дата и время передаются строкой в формате RFC 3339 в зоне UTC:
{
"created_at": "2026-09-15T17:30:00Z",
"updated_at": "2026-09-15T17:30:00Z"
}Буква Z означает нулевое смещение. Такой формат сортируется как строка, однозначно разбирается любой библиотекой и не зависит от настроек сервера.
§6Три схемы вместо одной
Обращение можно описать одной моделью и использовать её везде, но контракт от этого станет неточным: у создания, изменения и ответа разные наборы полей.
| Схема | Где используется | Чем отличается |
|---|---|---|
TicketCreate | тело POST | только title и description; лишние поля запрещены |
TicketUpdate | тело PATCH | все поля необязательны: клиент присылает то, что меняет |
Ticket | ответы | плюс id, status, created_at, updated_at — их заполняет сервер |
TicketPage | ответ на список | items, total, limit, offset |
class TicketCreate(BaseModel): model_config = ConfigDict(extra="forbid") title: str = Field(min_length=3, max_length=120) description: str = ""
Настройка extra="forbid" означает: поле, которого нет в схеме, — ошибка. Клиент, приславший свой id, получит 422 вместо тихого игнорирования. Это защищает от подмены серверных полей и от опечаток в именах.
§7Стиль проверяется линтером
Правило «свойства в snake_case» записано в .spectral.yaml проекта и применяется к описанию API целиком.
json-properties-snake-case:
description: Свойства JSON MUST быть записаны в snake_case.
severity: error
given: "$.components.schemas[*].properties"
then:
field: "@key"
function: casing
functionOptions:
type: snakeВот что печатает линтер, если в схему попало поле createdAt:
47:19 error json-properties-snake-case Свойства JSON MUST быть записаны
в snake_case.
components.schemas.Ticket.properties.createdAt
✖ 9 problems (7 errors, 2 warnings, 0 infos, 0 hints)Сообщение называет файл, строку и точный путь до нарушения. Правило записано заранее, поэтому его применение не зависит от того, кто смотрит изменение.
§8Частые ошибки
limit=500 «подрезали» до 100 и вернули данные. Клиент уверен, что получил всё.created_at рядом с updatedAt. Правило выбирают один раз и проверяют линтером.16.09.2026 21:30 в JSON — это строка, которую каждый клиент разберёт по-своему. Нужен RFC 3339 и UTC.id и created_at, и непонятно, кто их заполняет.§9Проверьте себя
- Где должен находиться идентификатор обращения и почему не в query?
- Запросите
?limit=500. Какой код вернёт сервис и что будет в теле? - Зачем в ответе на список нужны поля
total,limitиoffset? - Чем отличается
application/problem+jsonотapplication/jsonи когда используется каждый? - Что произойдёт, если в теле
POST /api/v1/ticketsпередать полеid? Почему так сделано?
path — идентификаторquery — фильтр, страницаheader — условия?limit=20&offset=0ответ: items, total,limit, offsetapplication/jsonapplication/problem+jsonдата — RFC 3339, UTCTicketCreate — входTicketUpdate — правкаTicket — ответextra="forbid"Справочник «Стандартизация HTTP API» · модуль 2 · глава 2.4Макаров Максим Николаевич