Модуль 2 · Правила HTTP

Справочник «Стандартизация HTTP API» · модуль 2 · правила HTTP

Глава 2.4

2.4

Параметры,
тело, форматы

Адрес и метод сказали, что делать. Остальное приходит параметрами и телом: фильтр, размер страницы, формат данных, имена полей. В этой главе разберём, где чему место, как устроена пагинация и почему стиль имён в JSON — часть контракта, а не вкус разработчика.

Категория
Правила HTTP
Уровень
Базовый
Связанные главы
1.3 Ресурсы · 2.3 Коды ответов · 3.2 Разделы OpenAPI · 6.2 Линтер
Зачем это знать
Чтобы клиент не выкачивал коллекцию целиком и не гадал, как называются поля

§1Четыре места параметра

Значение может приехать в запросе четырьмя путями, и выбор пути — часть контракта. В описании OpenAPI у параметра есть поле in, которое прямо называет место.

in: pathЧасть адреса, без которой ресурс не определён: /tickets/{ticket_id}. Всегда обязателен.идентификатор
in: queryУточнение выборки после «?»: status, limit, offset. Обычно необязателен и имеет значение по умолчанию.фильтр
in: headerСведения о сообщении и условия: Content-Type, If-Match, Idempotency-Key.условия
in: cookieЗначение, которое браузер хранит и посылает сам. В API для мобильных и серверных клиентов почти не используется.редко

Смешение ролей ломает смысл запроса. Идентификатор в 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
}
limitСколько объектов вернуть. Значение по умолчанию — 20, максимум — 100: верхняя граница защищает сервер от запроса «дай миллион».1…100
offsetСколько объектов пропустить от начала выборки. Вторая страница по 20 — это offset=20.≥ 0
totalСколько объектов подходит под фильтр всего. Клиент рисует по нему число страниц.в ответе

Запрос с 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, поймёт смысл полей.

Без Content-Type тело не читается

Отправив JSON без заголовка, вы получите ошибку, которая выглядит как «сервер не понял запрос». Сервер действительно не понял: он не обязан угадывать формат по первому символу.

§5Стиль JSON

Имена полей — часть контракта, и менять их потом нельзя. Поэтому стиль выбирают один раз и применяют ко всем ресурсам сразу.

наш выборsnake_case
  • created_at, ticket_id, in_progress;
  • совпадает со стилем Python;
  • проверяется правилом линтера.
другой вариантcamelCase
  • 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
app/schemas.py · запрет лишних полей
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 целиком.

.spectral.yaml · фрагмент
  json-properties-snake-case:
    description: Свойства JSON MUST быть записаны в snake_case.
    severity: error
    given: "$.components.schemas[*].properties"
    then:
      field: "@key"
      function: casing
      functionOptions:
        type: snake

Вот что печатает линтер, если в схему попало поле createdAt:

npx @stoplight/spectral-cli lint openapi.yaml
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Проверьте себя

  1. Где должен находиться идентификатор обращения и почему не в query?
  2. Запросите ?limit=500. Какой код вернёт сервис и что будет в теле?
  3. Зачем в ответе на список нужны поля total, limit и offset?
  4. Чем отличается application/problem+json от application/json и когда используется каждый?
  5. Что произойдёт, если в теле POST /api/v1/tickets передать поле id? Почему так сделано?
Места параметровpath — идентификаторquery — фильтр, страницаheader — условия
Пагинация?limit=20&offset=0ответ: items, total,limit, offset
Форматыapplication/jsonapplication/problem+jsonдата — RFC 3339, UTC
СхемыTicketCreate — входTicketUpdate — правкаTicket — ответextra="forbid"

Справочник «Стандартизация HTTP API» · модуль 2 · глава 2.4Макаров Максим Николаевич

Кадр