2026 г.

JSON и дизайн API

Проект ЦИТадель

В архиве нашего подраздела хранится большая коллекция материалов об XML — языке, на котором веб собирался говорить в начале века. В итоге основным форматом обмена данными стал JSON, а проектирование HTTP API превратилось в повседневную работу разработчика. Сначала разберём сам формат, включая отсутствующие в нём типы и соглашения, которыми они заменяются. Затем рассмотрим API как контракт: ресурсы и методы, идемпотентность, проверяемую схему, эволюцию без поломки клиентов и альтернативы — gRPC, GraphQL и события. В практикуме предстоит спроектировать и описать небольшой API библиотеки.

1. JSON: формат, победивший простотой

В JSON есть шесть видов значений: объект, массив, строка, число, логическое значение и null. Объект хранит пары «имя — значение», массив — упорядоченную последовательность. Синтаксис невелик, а средства разбора существуют почти во всех распространённых языках. Структуры JSON естественно отображаются на словари, списки и скалярные значения, поэтому при обычном обмене данными не приходится выбирать между элементом и атрибутом, как в XML. JSON менее выразителен, но для этой задачи часто оказывается достаточно его небольшой переносимой модели.

Важно сразу назвать то, чего в JSON нет. Каждый такой пробел заполняется соглашением, и обе стороны API должны понимать его одинаково:

  • Даты и время. Отдельного типа даты нет. Момент времени обычно передают строкой RFC 3339 с Z или явным смещением, например 2026-07-19T14:30:00Z. Календарная дата вроде дня рождения может не обозначать момент и записываться как 2026-07-19. Контракт должен различать эти случаи, а не добавлять часовой пояс к любой дате.
  • Целые числа и точность. Число в JSON не имеет заданной разрядности. Хорошая совместимость достигается в пределах IEEE 754 binary64; целые от −(253−1) до 253−1 разные реализации представляют точно. Поэтому 64-битные идентификаторы нередко передают строками. Для денег выбирают либо целое число минимальных единиц, либо десятичную строку и отдельно задают валюту, масштаб и округление.
  • Комментарии, ссылки и порядок членов объекта. Эти свойства не входят в переносимую модель JSON. Если порядок важен, данные представляют массивом. Ссылки между объектами задаются прикладным соглашением, а пояснения к полям помещаются в описание схемы.

2. HTTP API: словарь, который уже спроектирован

Хороший HTTP API использует семантику протокола, а не придумывает её заново. Транспортная сторона HTTP разобрана в статье «HTTP/2 и HTTP/3»; здесь важны три элемента прикладного контракта:

  • Ресурсы и имена. Основу API обычно составляют предметные сущности: /orders/17, а не /getOrderById. Иерархия уместна там, где она выражает устойчивую связь, например /orders/17/items. Идентификатор остаётся непрозрачным для клиента. Предметная команда тоже допустима, если она точнее описывает переход состояния, чем искусственное изменение набора полей.
  • Методы с готовой семантикой. GET и HEAD являются безопасными: клиент не просит изменить состояние ресурса. PUT обычно означает полную замену выбранного представления, PATCH — частичное изменение, POST — создание ресурса с назначаемым сервером адресом или выполнение команды, DELETE — удаление связи с ресурсом. GET, PUT и DELETE идемпотентны по предполагаемому эффекту: повтор не должен умножать действие, хотя ответ может отличаться. PATCH получает это свойство только тогда, когда его задаёт формат изменения.
  • Статусы с готовой семантикой. 201 сообщает о создании, 204 — об успехе без тела; 400 означает некорректный запрос, 401 — необходимость аутентификации, 403 — отказ в доступе, 404 — отсутствие ресурса, 409 — конфликт состояния, 429 — ограничение частоты. Класс 5xx указывает на ошибку сервера. Но правило повтора нельзя вывести только из первой цифры: 429 и 503 могут содержать Retry-After, а POST после обрыва связи опасно повторять без знания результата.

Если создание или другая операция не должны задваиваться, API может определить ключ идемпотентности. Клиент повторяет запрос с тем же ключом, а сервер связывает его с уже начатой или завершённой операцией. Контракт должен задать область уникальности ключа, срок хранения и поведение при другом теле с тем же значением. Тайм-ауты и повторы подробнее рассмотрены в статье «Надёжность поверх ненадёжной сети».

Тело ошибки тоже является частью контракта. RFC 9457 определяет Problem Details: машинно-разбираемый тип проблемы, краткий заголовок, статус, пояснение и адрес конкретного случая. Формат можно расширить, например списком ошибок полей, но внутреннюю трассу и закрытые сведения клиенту не передают.

3. Схема: документация, которую можно проверить

API связывает несколько программ и нередко несколько команд, поэтому контракт должен существовать в машиночитаемой форме. OpenAPI описывает пути, методы, параметры, тела и ответы HTTP API. Формы данных задаются Schema Object, основанным в OpenAPI 3.1 на диалекте JSON Schema: в нём выражаются типы, обязательные поля и ограничения.

Из одной спецификации инструменты могут получить интерактивную документацию, клиентские и серверные заготовки, проверку запросов и ответов и часть контрактных тестов. Это происходит не само по себе: соответствующие стадии нужно включить в сборку, а возможности генератора — проверить на целевом языке. Подход «сначала спецификация» и получение описания из кода оба работают, если документ остаётся источником истины и меняется вместе с реализацией. Иначе он быстро превращается в устаревшую справку. Эта дисциплина продолжает идеи статьи «Инженерный конвейер».

4. Практические конвенции: пагинация, фильтры, версии

  • Пагинация. Смещение, например ?page=5&size=20, позволяет перейти к номеру страницы, но на изменяющихся данных записи могут повторяться или пропадать между запросами; глубокое смещение бывает дорогим для хранилища. Курсор — непрозрачный маркер «продолжить после этого места» — лучше подходит последовательному чтению меняющегося набора. Для него нужен стабильный полный порядок. Смещение остаётся полезным там, где пользователю действительно нужны страницы с номерами.
  • Фильтрация и сортировка. Поля, операции и направления перечисляют в схеме. Переданный параметр нельзя напрямую превращать в SQL или другой внутренний язык запроса: значения связывают параметрами, а имена выбирают из разрешённого набора.
  • Версионирование. Добавление необязательного поля часто совместимо, если клиенты игнорируют неизвестные члены. Переименование, удаление или смена смысла обычно требуют новой версии и переходного периода. Версию можно поместить в путь, заголовок или тип содержимого; путь вида /v2/ проще всего увидеть и маршрутизировать. Старую версию отключают после объявления замены и срока, публикации способа миграции и измерения оставшегося использования.

5. Альтернативы: gRPC, GraphQL, события

JSON поверх HTTP — распространённое исходное решение, но не обязательное. Альтернативы устраняют определённые затруднения и приносят собственные требования:

  • gRPC описывает сервис и сообщения в Protocol Buffers, генерирует типизированные интерфейсы и поддерживает потоковые вызовы. Двоичное представление часто компактнее JSON, хотя выигрыш зависит от данных и сжатия. gRPC удобен для связей между управляемыми одной организацией сервисами, где схема и генерация кода естественны. В браузере обычно нужен gRPC-Web и прокси, поэтому на внешней HTTP-границе нередко сохраняют JSON.
  • GraphQL позволяет клиенту выбрать поля и связи одной операцией. Он уменьшает недобор и перебор данных при множестве разных представлений, но сервер должен ограничивать глубину и стоимость запросов, пакетировать доступ к данным и проверять права на уровне полей. По HTTP сервер принимает POST и может принимать GET для операций чтения, поэтому кэширование возможно, но требует отдельного проектирования. Для небольшого CRUD API эта сложность может не окупиться.
  • События нужны, когда потребителю важно узнавать о произошедшем, а не постоянно опрашивать API. Вебхук — исходящий HTTP-запрос к потребителю; брокер хранит и распределяет сообщения по собственной модели. В обоих случаях определяют повторы, идентификатор события, подпись или другой способ проверки отправителя и идемпотентную обработку. Журналы и доставка событий разобраны в главе 10 курса о распределённых системах.

6. Практикум: API по всем правилам

Сквозная задача — небольшой API библиотеки: книги, читатели и выдачи. Понадобятся выбранный язык, редактор OpenAPI и curl или другой HTTP-клиент.

Задание 1. Дизайн на бумаге. Спроектируйте ресурсы и пути. Решите, будет ли выдача самостоятельным ресурсом или подресурсом читателя, и обоснуйте выбор. Назначьте методы и статусы, включая «выдать книгу» и «продлить выдачу». Для создания предусмотрите защиту от задвоения после неопределённого сетевого исхода.

Задание 2. Спецификация. Опишите решение в OpenAPI: схемы сущностей, строковые идентификаторы, календарные даты и моменты времени, тела ошибок по RFC 9457 и курсорную пагинацию списка книг. Сгенерируйте документацию и проверьте, достаточно ли её для реализации клиента без устных пояснений.

Задание 3. Реализация и проверка. Реализуйте две или три операции с валидацией запросов по схеме. Проверьте корректный сценарий, ошибку валидации и повтор создания с тем же ключом идемпотентности. Повтор не должен создать второй ресурс; статус и тело определяются записанным контрактом и не обязаны совпадать побайтно.

Задание 4. Эволюция без поломки. Добавьте необязательное поле и убедитесь, что прежний клиент продолжает работать. Затем придумайте несовместимое изменение, например переименование поля, и оформите новую версию, период сосуществования и план отключения старой.

Задание 5*. Сравнение вариантов. Опишите операцию «выдать книгу» в Protocol Buffers, а получение читателя с его выдачами и книгами — в GraphQL. Для каждого варианта перечислите, что он упрощает и какие требования добавляет на этом небольшом примере.

Итоги

  • JSON удобен небольшой переносимой моделью. Даты, большие идентификаторы, деньги, ссылки и другие отсутствующие понятия требуют явных соглашений.
  • HTTP уже задаёт семантику методов и статусов. Повтор операции зависит от её идемпотентности, результата предыдущей попытки и контракта API.
  • Спецификация OpenAPI становится источником истины, когда из неё получают документацию и проверки, а её соответствие реализации контролирует сборка.
  • Курсор и смещение решают разные задачи. Новая версия нужна для несовместимого изменения, а отключение прежней требует переходного периода и измерений.
  • gRPC, GraphQL, вебхуки и брокеры выбирают по свойствам конкретной границы, а не как общий следующий этап после JSON.

Литература

  1. RFC 8259: The JavaScript Object Notation, RFC 9110: HTTP Semantics и RFC 9457: Problem Details for HTTP APIs.
  2. OpenAPI Specification 3.1.1 и JSON Schema 2020-12.
  3. Google API Improvement Proposals и Microsoft REST API Guidelines — справочники практических соглашений.
  4. Оглавление раздела «Веб-технологии»; другие статьи цикла: «Как устроен браузер», «Современный JavaScript и TypeScript», «Как устроен фронтенд-фреймворк» и «Веб-производительность».
404 Not Found

404 Not Found


nginx/1.24.0 (Ubuntu)

Связь с редакцией