Действия, не вписывающиеся в CRUD
Урок о том, что делать с операциями, которые не сводятся к «создать/прочитать/изменить/удалить».
CRUD-разрыв — ситуация, когда у бизнеса есть действие («опубликовать», «отменить», «пересчитать»), для которого нет очевидного существительного-ресурса и стандартного метода.
REST красиво ложится на данные: сущности — это существительные, а GET/POST/PATCH/DELETE закрывают чтение и правку. Но реальные системы полны процессов: заказ отменяют, статью публикуют, корзину оформляют, счёт пересчитывают. Соблазн велик — добавить POST /orders/42/cancel и забыть про чистоту. Иногда это даже правильный ответ. Но прежде чем тянуться к глаголу, стоит проверить три более «ресурсных» приёма — часто действие удаётся выразить как изменение состояния, и API остаётся стройным.
Приём 1: действие — это изменение поля (флаг через PATCH)
Многие «действия» на самом деле меняют состояние ресурса. «Опубликовать статью» = перевести её status из draft в published. А смена поля — это PATCH элемента, штатная CRUD-операция. Глагол не нужен вовсе.
# Вместо POST /articles/15/publish
curl -X PATCH https://api.example.com/articles/15 \
-H "Content-Type: application/json" \
-d '{"status": "published"}'
Такой подход хорош, когда действие сводится к одному-двум полям и не запускает сложный побочный процесс. Состояние ресурса становится явным и читаемым: клиент видит status в ответе и понимает, что доступно дальше.
| Действие | Как флаг |
| опубликовать | PATCH /articles/15 → {"status":"published"} |
| архивировать | PATCH /projects/7 → {"archived":true} |
| включить уведомления | PATCH /settings → {"notify":true} |
Приём 2: действие — это под-ресурс (POST в коллекцию)
Если действие порождает сущность с собственной жизнью — у него есть время, причина, инициатор, статус — его лучше смоделировать как новый ресурс. «Отменить заказ» создаёт отмену: у неё есть дата, причина, кто отменил. Тогда это POST в под-коллекцию.
curl -X POST https://api.example.com/orders/42/cancellation \
-H "Content-Type: application/json" \
-d '{"reason": "changed mind", "by": "user"}'
{
"id": 301,
"order_id": 42,
"reason": "changed mind",
"by": "user",
"created_at": "2026-06-22T10:14:00Z"
}
Теперь отмена адресуема (GET /orders/42/cancellation), её можно прочитать, она хранит контекст. Это куда богаче, чем глагол /cancel, который ничего не оставляет после себя. Тот же приём: «вернуть деньги» → POST /orders/42/refunds, «лайкнуть пост» → POST /posts/15/likes, «отправить письмо повторно» → POST /messages/9/deliveries.
Как выбрать между флагом и под-ресурсом
- Флаг (PATCH) — если действие меняет состояние самого ресурса и не нужно хранить историю каждого срабатывания.
- Под-ресурс (POST) — если у действия есть собственные атрибуты, история, и оно может повторяться (несколько возвратов по заказу).
Приём 3: controller-ресурс (прагматичный компромисс)
Остаются действия, которые не меняют одно поле и не создают красивую сущность: «пересчитать корзину», «проверить промокод», «сконвертировать валюту», «перестроить индекс». Это вычисления-процессы. Для них существует признанный паттерн — controller-ресурс: глагол-действие как последний сегмент, вызываемый через POST.
POST /carts/9/recalculation # пересчитать корзину
POST /invoices/77/recalculate # пересчитать счёт
POST /search # сложный поиск с телом-запросом
POST /currency/conversions # сконвертировать (тело: from, to, amount)
Это сознательное отступление от «только существительные», и оно легитимно: даже сторонники строгого REST признают, что не всякий процесс — это ресурс. Главное — соблюдать дисциплину: такие ручки всегда POST (они не идемпотентны и не кэшируются), их немного, и они выделены как явное исключение, а не как стиль всего API. Существительное-отглагольное (recalculation) читается чуть лучше глагола (recalculate), но это уже вкусовщина.
| Природа действия | Приём | Пример |
| смена состояния | флаг через PATCH | PATCH /articles/15 |
| событие с историей | под-ресурс POST | POST /orders/42/cancellation |
| вычисление/процесс | controller-ресурс POST | POST /carts/9/recalculation |
Как работает под капотом
Почему именно POST для действий-исключений? Дело в гарантиях методов. HTTP делит методы по двум свойствам: безопасность (не меняет состояние) и идемпотентность (повтор даёт тот же результат).
Метод Безопасный Идемпотентный Подходит для действия?
GET да да нет — не должен менять состояние
PUT нет да да, если повтор безопасен
PATCH нет нет* да, для смены полей
POST нет НЕТ да — для не идемпотентных действий
«Пересчитать» или «отменить» — действия не идемпотентные или порождающие новую сущность каждый раз, поэтому им подходит POST, который ничего и не обещает про повтор. Ставить такое на GET опасно: браузеры, прокси и краулеры свободно повторяют и кэшируют GET, и однажды поисковый робот, обойдя ссылки, может «отменить» сотню заказов. Именно поэтому действие никогда не вешают на GET — даже если технически это сработает.
Для не идемпотентных действий, которые нельзя повторять (списание денег), на сервере добавляют ключ идемпотентности — клиент шлёт заголовок Idempotency-Key, сервер запоминает его и на повторный запрос с тем же ключом возвращает прежний результат, не выполняя действие дважды. Это компенсирует то, что POST сам по себе не идемпотентен.
Частые ошибки
- Глагол по привычке.
POST /publishArticle, когда хватило быPATCH /articles/15со сменойstatus. Сначала проверьте, не смена ли это состояния. - Действие на GET.
GET /orders/42/cancel— катастрофа: краулеры и прокси повторят и закэшируют. Действия — только небезопасные методы (POST/PATCH). - Зоопарк controller-ручек. Если глаголов-исключений становится больше, чем ресурсов, вы строите RPC, а не REST. Держите их редкими и явными.
- Потеря контекста. Глагол
/cancelне хранит причину и время. Если это важно — моделируйте как под-ресурсcancellation. - Нет защиты от повтора. Платёжные действия без
Idempotency-Keyпри ретрае спишут деньги дважды.
Итоги
- Прежде чем добавлять глагол, проверьте: не смена ли это состояния (тогда
PATCHполя) и не событие ли с историей (тогда под-ресурсPOST). - Под-ресурс делает действие адресуемым и хранит контекст:
POST /orders/42/cancellation. - Controller-ресурс — легитимный компромисс для вычислений-процессов, но всегда
POSTи в малых дозах. - Действие никогда не вешают на
GET— его повторяют и кэшируют без спроса. - Для неповторяемых действий (платежи) добавляйте
Idempotency-Key.