HTTP-методы и их семантика

Урок про то, какой контракт несёт каждый HTTP-метод и почему правильный выбор метода важнее красивого URL.

HTTP-метод (глагол) — это объявленное намерение клиента: что он хочет сделать с ресурсом, на который указывает URL. Метод — часть контракта API наравне с самим адресом.

В REST URL отвечает на вопрос «какой ресурс», а метод — на вопрос «что с ним сделать». Один и тот же адрес /orders/42 ведёт себя совершенно по-разному в зависимости от глагола: GET его читает, PUT заменяет, DELETE удаляет. Поэтому в грамотном API не бывает URL вида /getOrder или /deleteOrder — глагол уже зашит в метод, дублировать его в адресе и избыточно, и опасно (легко рассинхронизировать).

Зачем вообще соблюдать семантику методов, если технически сервер может на GET хоть удалять записи? Затем, что вокруг HTTP выстроена целая инфраструктура, которая верит контракту: браузеры предзагружают GET-ссылки, прокси кэшируют ответы GET, поисковые роботы свободно ходят по GET-адресам, балансировщики безопасно повторяют запросы. Нарушив семантику, вы получаете спецэффекты: робот, обойдя страницу со ссылками-удалялками на GET, может вычистить вам половину базы.

GET — чтение ресурса

GET запрашивает представление ресурса и ничего не меняет на сервере. Тела запроса у него, как правило, нет — все параметры передаются в URL (path и query-string). Успешный ответ — 200 OK с телом-представлением; если ресурса нет — 404 Not Found.

curl -i https://api.shop.test/orders/42
curl -i "https://api.shop.test/orders?status=paid&limit=20"

Коллекция (/orders) и отдельный элемент (/orders/42) — это два разных ресурса, и GET к ним возвращает разные представления: список и единичный объект.

POST — создание и нестандартные операции

POST отправляет данные на обработку: чаще всего — создаёт новый подчинённый ресурс внутри коллекции. Тело запроса несёт создаваемый объект. Сервер сам присваивает идентификатор и возвращает 201 Created, а в заголовке Location — адрес новинки.

curl -i -X POST https://api.shop.test/orders \
  -H "Content-Type: application/json" \
  -d '{"item": "keyboard", "qty": 2}'
HTTP/1.1 201 Created
Location: /orders/43
Content-Type: application/json

Ключевая черта POST — клиент не знает URL будущего ресурса заранее, его назначает сервер. POST также используют как «универсальный» глагол для операций, не ложащихся в CRUD: POST /orders/43/refund, POST /search со сложным телом-запросом.

PUT vs PATCH — полная замена против частичного изменения

Оба меняют существующий ресурс, но по-разному. PUT — это полная замена. Клиент присылает ресурс целиком, и сервер делает его таким, как в теле. Поля, которых нет в теле, трактуются как отсутствующие (обнуляются или сбрасываются на дефолт). Важно: при PUT клиент сам владеет URL ресурса.

curl -i -X PUT https://api.shop.test/orders/42 \
  -H "Content-Type: application/json" \
  -d '{"item": "mouse", "qty": 1, "status": "paid"}'

PATCH — частичное обновление. Клиент присылает только те поля, которые надо изменить; остальные сервер не трогает.

curl -i -X PATCH https://api.shop.test/orders/42 \
  -H "Content-Type: application/json" \
  -d '{"status": "shipped"}'

Разница в семантике видна на примере. Допустим, заказ {"item":"mouse","qty":1,"status":"paid"}. Если прислать PUT с телом {"status":"shipped"} — по контракту поля item и qty исчезнут (тело и есть новый ресурс). А PATCH с тем же телом поменяет только status, сохранив остальное. Поэтому для «поменять один флажок» берут PATCH, а для «загрузить заново весь объект» — PUT.

DELETE — удаление ресурса

DELETE /orders/42 удаляет ресурс. Тела запроса обычно нет. Успех — 200 OK (если возвращаем тело-итог) или 204 No Content (если тела нет). Повторный DELETE уже удалённого ресурса корректно отвечает 404 — самого ресурса больше нет, но это не ошибка клиента в смысле «он сделал что-то не так».

HEAD и OPTIONS кратко

HEAD — то же, что GET, но сервер возвращает только заголовки, без тела. Удобно проверить существование ресурса, его размер (Content-Length) или дату изменения, не качая мегабайты. OPTIONS сообщает, какие методы допустимы для ресурса — ответ несёт заголовок Allow: GET, POST. Именно OPTIONS браузер шлёт автоматически как preflight-запрос при CORS.

Как работает под капотом

Метод — это первое слово стартовой строки HTTP-запроса. Сырой POST выглядит так:

POST /orders HTTP/1.1
Host: api.shop.test
Content-Type: application/json
Content-Length: 31

{"item": "keyboard", "qty": 2}

Сервер читает первое слово и маршрутизирует запрос. Во фреймворках это видно в декларации роутов: метод и путь вместе образуют ключ маршрута.

GET    /orders          -> список заказов
POST   /orders          -> создать заказ
GET    /orders/{id}      -> один заказ
PUT    /orders/{id}      -> заменить заказ
PATCH  /orders/{id}      -> частично обновить
DELETE /orders/{id}      -> удалить заказ

Один и тот же путь с разными методами — это разные обработчики. Если на путь не нашлось обработчика под нужный метод, сервер обязан вернуть 405 Method Not Allowed и перечислить разрешённые методы в заголовке Allow.

Частые ошибки

  • Глаголы в URL: POST /createOrder, GET /deleteOrder?id=42. Действие должно жить в методе, а не в адресе.
  • POST вместо GET для чтения, потому что «нужно передать много фильтров». Вы теряете кэширование и закладки. Для чтения берите GET с query-параметрами.
  • PUT как PATCH: прислать в PUT одно поле и удивиться, что остальные обнулились. Если меняете часть — это PATCH.
  • DELETE с обязательным телом: многие клиенты и прокси не пересылают тело у DELETE. Идентификатор кладите в URL.

Итог

  • URL отвечает «какой ресурс», метод — «что сделать»; не дублируйте глагол в адресе.
  • GET читает, POST создаёт (URL назначает сервер, ответ 201 + Location).
  • PUT — полная замена ресурса целиком, PATCH — изменение только присланных полей.
  • DELETE удаляет; HEAD — заголовки без тела, OPTIONS — список допустимых методов.
Проверьте себя
1. Заказ хранит {"item":"mouse","qty":1,"status":"paid"}. Клиент шлёт PUT /orders/42 с телом {"status":"shipped"}. Что произойдёт по контракту?
AИзменится только status, остальные поля сохранятся
BРесурс станет {"status":"shipped"}, поля item и qty исчезнут
CСервер отклонит запрос как неполный
DСервер вернёт 405, потому что PUT нельзя по этому пути
2. Клиент создаёт ресурс через POST /orders, не зная заранее его id. Что корректно вернуть?
A200 OK без заголовков
B204 No Content
C201 Created с заголовком Location, указывающим на новый ресурс
D301 Moved Permanently
3. Для чего предназначен метод HEAD?
AУдалить заголовки ресурса
BПолучить только заголовки ответа без тела (например, проверить существование или размер)
CСоздать ресурс без тела
DУзнать список разрешённых методов