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— список допустимых методов.