Начальный · Глава 1 / 18
Сначала договор, затем обработчик
После главы: Описать запрос и ответ так, чтобы клиент не угадывал поведение.
В этой главе
Операция глазами читателя
Читатель нажал «Зарезервировать книгу». Серверу недостаточно получить произвольный JSON: нужно знать, какую книгу, сколько экземпляров и от чьего имени резервируют. Контракт связывает вход, допустимые состояния и наблюдаемый результат. Начните с одного предложения: авторизованный читатель резервирует от одного до пяти доступных экземпляров; сервер возвращает номер резерва либо объяснимый отказ. Такое предложение уже помогает исключить несколько неоднозначностей.
Разделите транспорт и предметную задачу. HTTP переносит сообщение, а правило «не больше пяти» принадлежит библиотеке. Имя функции может измениться без изменения договора. Напротив, переход от количества в штуках к упаковкам меняет смысл даже при прежнем имени поля. Поэтому перечисляйте единицы, обязательность полей и ограничения явно.
Предсказуемый ответ
У запроса есть метод, адрес, заголовки и тело. GET предназначен для чтения представления ресурса; создание резерва в нашем договоре выполняется POST. Успешное создание возвращает 201, а клиент получает стабильный идентификатор. Это конкретный выбор API, а не требование называть каждый метод по таблице в базе.
Не отдавайте внутреннюю запись целиком. Выберите публичные поля: номер резерва, статус, количество. Владелец проверяется отдельно. Временная диагностическая колонка, появившаяся в базе, не должна автоматически попасть наружу. Прежде чем писать реализацию, составьте по одному примеру успеха, некорректного входа и запроса к чужому резерву.
Изменение договора и границы ресурса
Клиент старой версии знает только bookId и quantity. Добавление необязательного поля ответа обычно проще, чем изменение смысла quantity; даже новый статус ломает клиента с закрытым перечислением. Запишите допустимость неизвестных полей, диапазон размеров и политику совместимости. В учебном ответе Location указывает адрес созданного резерва.
При чтении списка ограничьте размер страницы. При чтении одной записи проверяйте пользователя, объект и действие, а при изменении — ещё и допустимые поля. Проверка владельца не разрешает клиенту менять внутренний статус. Контракт JSON сам по себе не предоставляет защиту cookie-сессии от CSRF; эту границу разбирает том о безопасности.
Уточнить гарантию на последовательности событий
Контракт полезно проверять не только по одному запросу, но и по последовательности действий. Создание резерва, чтение, отмена и повтор отмены образуют маленький жизненный цикл. Если каждый маршрут описан отдельно, может остаться неопределённым вопрос, что читать после отмены или сколько времени хранится запись. Добавьте историю из четырёх шагов и ожидаемое состояние после каждого. Она обнаруживает противоречия между формально правильными примерами отдельных маршрутов.
Идентификатор ресурса отличается от ключа попытки. Номер резерва обозначает уже созданную запись, а ключ повтора связывает несколько доставок одного намерения ещё до получения номера. Иногда клиент знает только ключ, потому что первый ответ потерян. Поэтому договор должен объяснять, как восстановить идентификатор и в какой области действителен ключ. Простая случайность строки не определяет её назначение.
Совместимость оценивается с точки зрения реальных клиентов. Добавление поля может оказаться несовместимым, если клиент запрещает неизвестные свойства; новый статус может нарушить закрытый switch. Перед изменением полезно запустить старые примеры запросов против новой версии и проверить ожидаемые обязательные поля. Версия в пути не исправляет семантическую неоднозначность сама по себе: нужно определить, какие обещания изменились и как долго старый договор поддерживается.
Наконец, документируйте пределы. Максимальное количество, размер страницы, срок хранения ключа и формат идентификатора влияют на клиента не меньше, чем имена полей. Не обещайте бесконечное хранение истории только потому, что пока нет очистки. Удаление данных в будущем должно учитывать восстановление неизвестного исхода и объяснимость ранее принятых действий.
Разобранный пример
Небольшой паспорт операции
Ниже схема обмена, а не исполняемый сетевой запрос. Сервер сам назначает номер и связывает резерв с текущим читателем. Клиент не присылает ownerId, цену или внутренний статус. В примере ровно два экземпляра; идентификатор — вымышленный.
textPOST /reservations
Content-Type: application/json
{"bookId":"bk-42","quantity":2}
201 Created
Location: /reservations/rs-17
{"id":"rs-17","status":"reserved","quantity":2}Теперь самостоятельно
Практика
Спроектируйте чтение одного резерва. Запишите путь, успешный ответ и поведение для неизвестного номера. Затем объясните, почему знание номера ещё не даёт права читать резерв.
Результат: Три коротких примера обмена и правило проверки владельца.
Подсказка
- Сначала найдите текущего читателя, затем проверяйте принадлежность.
Решение и проверка
Подходит GET /reservations/rs-17 с ответом 200 и разрешёнными полями. Неизвестный резерв возвращает 404. Для чужого резерва можно также выбрать 404, чтобы не раскрывать его существование; этот выбор документируют.
Право проверяется сервером в каждом чтении. Спрятанная ссылка в интерфейсе не является контролем доступа. Список полей ответа не включает внутренние заметки сотрудника.
Проверьте себя без текста
Почему одинаковый JSON не гарантирует одинаковый контракт?
Могут различаться единицы, правила доступа, допустимые состояния и обещанный результат.
Что означает 201 в этом примере?
Новый резерв создан; это не означает, что товар оплачен или выдан.
Клиент прислал status=issued в корректном JSON. Можно ли его сохранить?
Нет: допустимые поля и переход состояния определяет сервер, даже для владельца резерва.
Проверить по первоисточникам
Конец образца
Полный том содержит 18 глав, решения, итоговый проект и словарь. Продажи откроются после предметной редактуры и подключения магазина.