Перейти к содержанию

Методы и смысл операции

В первом уроке мы прочитали представление курса. Теперь добавим изменения каталога и выберем для них методы HTTP. Метод сообщает общий смысл операции ещё до чтения её тела. Поэтому недостаточно придумать красивый путь: нужно согласовать действие с ожиданиями клиента, посредников и человека, который будет поддерживать API.

Результатом урока станет небольшая таблица операций нашего сервиса. Не будем превращать каждую команду в отдельный глагол внутри URL. Сохраним коллекцию /api/courses и адрес записи /api/courses/c001, а смысл выразим методом и согласованными данными. Все изменения ниже происходят внутри самостоятельного учебного сценария, начинающегося с четырёх курсов снимка s1.

Чтение не должно редактировать

GET выбираем, когда клиент получает представление ресурса. Наш запрос курса JavaScript возвращает его поля, но не удаляет и не публикует запись. В частности, путь вроде /api/courses/c001?delete=true с методом GET был бы плохим договором: переход по ссылке скрыто изменял бы предметное состояние.

Безопасность метода здесь означает отсутствие запрошенного изменения ресурса, а не отсутствие всех побочных действий компьютера. Сервер способен записать технический журнал чтения. Это не даёт ему права считать GET командой удаления. Такое различие важно для автоматических переходов, предварительных запросов и программ, которые могут повторять чтение без отдельного согласия человека. Назначение методов HTTP.

HEAD нужен для получения метаданных без содержимого ответа. Например, клиенту интересны тип или валидатор курса перед загрузкой. Это не сокращённый JSON с одним полем: тело у ответа HEAD отсутствует. Поля в целом соответствуют GET; некоторые сведения, требующие генерации тела, сервер может не вычислять отдельно.

HEAD /api/courses/c001 HTTP/1.1
Host: catalog.example
Accept: application/json

Предполагаемый успешный ответ показывает Content-Type: application/json, но не содержит объект курса. Клиент не должен пытаться разобрать пустое тело как JSON. И GET, и HEAD относятся к чтению; различается потребность в передаче содержимого, а не право изменить каталог.

Создание в коллекции

Для создания записи, идентификатор которой выбирает сервер, используем POST к коллекции. Клиент задаёт редактируемые поля и не отправляет id. Входной тип описывает Content-Type, а желаемый выход — Accept.

POST /api/courses HTTP/1.1
Host: catalog.example
Content-Type: application/json
Accept: application/json

{"title":"HTTP и API","topic":"backend","lessons":32}

По нашему договору сервер создаёт c005, возвращает 201 и сообщает её адрес в Location. Этот адрес становится устойчивой ссылкой на новую запись. Название курса может измениться; ссылка продолжает указывать на его идентификатор.

HTTP/1.1 201 Created
Location: /api/courses/c005
Content-Type: application/json

{"id":"c005","title":"HTTP и API","topic":"backend","lessons":32}

POST имеет более общее назначение обработки предоставленного содержимого. Сам протокол не обещает, что любой POST создаёт запись. Здесь создание — выбранный договор коллекции. В другом API POST может запускать расчёт или принимать форму. Поэтому документация пути остаётся необходимой даже после выбора знакомого метода.

Если тот же запрос создания отправить повторно без дополнительных правил, сервис может создать ещё один курс. Это отличается от повторного чтения. Поведение после потери ответа станет темой идемпотентности; сейчас запомним, что форма сообщения не доказывает безопасность его повторения.

Замена известного состояния

Для полной замены редактируемого представления существующей записи выберем PUT. Идентификатор уже известен и находится в пути. Все три редактируемых поля обязательны; пропуск lessons не означает «оставить как раньше».

PUT /api/courses/c001 HTTP/1.1
Host: catalog.example
Content-Type: application/json
Accept: application/json

{"title":"JavaScript для веба","topic":"frontend","lessons":40}

Ожидаемое предметное состояние — c001 с новым заголовком и прежними значениями двух явно отправленных полей. Серверный идентификатор не заменяется телом. Наше редактируемое представление специально ограничено тремя полями: внутренняя дата аудита не относится к входному документу и не обязана стираться.

В общем случае PUT может создавать представление по известному клиенту адресу. Наш API разрешает только замену существующей записи: неизвестный c999 даёт 404. Это ограничение нужно объявить заранее; нельзя вывести его из одной буквы метода. Разделение POST для назначения нового id и PUT для замены упрощает учебную модель.

До урока об условных изменениях здесь показана только семантика замены. Позже добавим обязательный If-Match, чтобы редактор не перезаписывал чужую новую версию. Сам PUT ещё не решает конфликт двух одновременно открытых форм. Его идемпотентность и защита от потери обновления — разные свойства.

Полнота документа и намерение формы

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

Такой клиент не должен брать количество уроков из давно загруженной случайной карточки. Данные формы должны иметь понятное происхождение, а перед отправкой пригодится условие версии. Если разработчик хочет отправлять только изменённое название, лучше отдельно спроектировать частичное изменение, чем сделать PUT зависимым от догадок об отсутствующих ключах. Выбор метода отражает договор, а не число полей на экране.

PATCH тоже может быть идемпотентным или неидемпотентным в зависимости от формы изменения. Команда «установить lessons равным 40» отличается от «увеличить lessons на один». Первая при повторе сохраняет то же значение, вторая меняет его снова. Поэтому будущий выбор PATCH потребует описания операции и формата, например документа присваиваний, а не только поддержки нового метода в маршрутизаторе.

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

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

Удаление и неподдерживаемый метод

DELETE к адресу записи означает удаление её связи с текущим функционалом API. В нашем каталоге после принятого удаления последующее чтение даёт 404. Не следует обещать физическое уничтожение резервных копий или журналов: это отдельные правила хранения системы.

Для принятого удаления выбираем 204 без тела. Если интерфейс хочет узнать изменённый список, он делает новое чтение коллекции. Это яснее, чем пытаться получить JSON из ответа, который по определению не несёт содержимого.

Для частичного изменения существует PATCH, но он требует отдельного договора о формате изменения. Нельзя просто назначить PATCH прежнему телу и надеяться, что все клиенты одинаково поймут пропущенные поля и null. Пока наша запись не поддерживает этот метод. Предполагаемый ответ на него:

HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD, PUT, DELETE
Content-Type: application/problem+json

{"type":"https://catalog.example/problems/method-not-allowed","title":"Метод не поддерживается","status":405}

Allow перечисляет поддерживаемые методы именно цели /api/courses/c001. Коллекция имеет другой набор: чтение и создание. Возможность метода не равна праву каждого пользователя: даже разрешённый PUT потребует прав редактора. В текущих схемах считаем их установленными заранее, а не создаём мнимый токен доступа.

Теперь для каждого действия можно назвать адрес, метод и изменяемое состояние. Попробуйте описать переименование через POST и PUT и объяснить, почему в нашем договоре второй вариант точнее. Не ищите ответ только в названии метода: проверьте, кто назначает идентификатор и является ли тело полной заменой. Следующий урок уточнит, какие статусы сообщают о разных исходах этих операций.

Оглавление курса