
Введение
Привет, Хабр! Каждый раз, создавая новый эндпоинт, я ловил себя на мысли: «А как назвать маршрут?». Казалось, что где-то есть законы и правила, которые помогают создавать API последовательно. Со временем я наткнулся на диаграммы по проектированию, прочитал книгу JJ Geewax — API Design Patterns (Джей‑Джей Гивакс), изучил рекомендации от крупных компаний и понял важную мысль: проектирование API — это такая же область знаний со своими принципами и стандартами.
Следование этим правилам даёт практический результат:
API становятся удобными для разработчиков благодаря единообразным паттернам и стандартам веба.
Бизнес‑логику и сам API легче переиспользовать.
Интерфейс остаётся понятным и предсказуемым для внешних клиентов.
Однако возникает проблема: стандарты есть, но они разные. Многие из них красивы на бумаге, но непонятно, как их применить в обычном CRUD‑приложении без сложной бизнес‑логики.
Цель статьи — дать компактную шпаргалку по проектированию API для простых CRUD‑сервисов и показать ход мыслей, который позволяет проектировать последовательно и осмысленно.
Часть 1. Всё начинается с домена и кода
В хорошем API сначала проектируются ресурсы и взаимодействие с ними. Если доменная модель и её операции ясны, HTTP‑слой становится простым и предсказуемым.

Минимальный словарь действий
Чтобы достичь ожидаемого результата, опираемся на пять базовых операций в бизнес‑логике:
GetResource — получить конкретный ресурс.
ListResources — получить коллекцию ресурсов (с фильтрами/сортировкой/пагинацией).
CreateResource — создать ресурс.
UpdateResource — изменить ресурс (частично или полностью).
DeleteResource — удалить ресурс.
Чего не должно быть: GetByOrganization
, GetByUser
, Insert
, Upsert
, DeleteAll
, FindOrCreate
, ArchiveOldResources
и т. д.
Вся вариативность — в параметрах, а не в новых именах методов.
Единообразный нейминг делает навигацию в коде очевидной.
Фильтр вместо
GetBy*
убирает дубли и взрыв количества эндпоинтов.
Часть 2. Стандартные методы API
Это те методы, на которые стоит посмотреть в первую очередь.

Ресурс: Resource
Модель для примера:
{
"id": "guid",
"title": "string",
"status": "active|archived|draft"
}
1) Создание
POST /v1/resources
Тело запроса:
{ "title": "New resource", "status": "draft" }
Ответ 201 + созданный ресурс:
{ "id": "guid", "title": "New resource", "status": "draft" }
2) Получение конкретного ресурса
GET /v1/resources/{id}
Ответ 200 + ресурс; 404 — не найден.
3) Получение всех ресурсов
GET /v1/resources
Ответ 200:
{
"resources": [
{ "id": "guid", "title": "New resource", "status": "draft" }
]
}
Возвращаем объект, а не «голый массив» — так проще расширять контракт (метаданные, пагинация и т. д.).
При отсутствии ресурсов возвращаем 200 и пустую коллекцию.
4) Полная замена (replace)
PUT /v1/resources/{id}
Тело запроса — полное представление ресурса (всё, что должно остаться в состоянии после замены):
{ "title": "Updated", "status": "active" }
Коды ответа:
201 — созданный ресурс (если позволяем клиенту создавать со своим идентификатором; иначе — 404).
200 — актуальный ресурс (если он был и мы его заменили).
Если поле не прислано, оно должно принять значение по умолчанию.
5) Частичное обновление
PATCH /v1/resources/{id}
Тело запроса — только изменяемые поля:
{ "status": "archived" }
Ответ 200 — актуальный ресурс; 404 — не найден.
6) Удаление
DELETE /v1/resources/{id}
Ответ 204 (без тела).
Рекомендации по разделу
Всегда множественное число сущности в путях:
/resources
.Для стандартных операций идентификатор — только в пути:
/resources/{id}
.Возвращаем полный ресурс на POST/PUT/PATCH — так проще дебажить, тестировать и поддерживать.
Часть 3. Расширение стандартных методов
Теперь усилим базовые операции — без размножения эндпоинтов и версий.

Пагинация
Самая простая и распространённая — offset‑based. Параметры: skip
, take
(синонимы: offset
, limit
).
GET /v1/resources?skip=20&take=10
— пропускаем 20, берём 10.
Ответ:
{
"resources": [
{ "id": "guid", "title": "..." }
],
"total": 350
}
total
в ответе помогает клиенту понять, есть ли смысл тянуть следующие страницы.Сортируем результат хотя бы по дате добавления, если пользователь не указал иного, чтобы возвращать детерминированные данные.
Обязательно фиксируем максимальный
take
.Если выборка нестабильна, огромна, с тяжёлой сортировкой — используем cursor‑based пагинацию.
См. также:
Microsoft REST API Guidelines — collections: https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#collections
Google API Design Guide — Standard methods: List: https://google.aip.dev/132
Фильтрация
Чтобы не уходить в парсинг, берём прямой путь: явные query‑параметры на каждый фильтр.
Суффиксы операторов:
_eq
(по умолчанию, можно опустить),_ne
,_lt
,_gt
,_lte
,_gte
_in
,_nin
— для множеств
Для вложенных полей — «плоские» имена: meta_created_at_gte=...
Примеры:
GET /v1/resources?status_in=active,draft
GET /v1/resources?title_eq=Design
GET /v1/resources?meta_created_at_gte=2023-01-01&meta_created_at_lte=2023-06-01
Для фильтрации по вложенным массивам сложных объектов (например,
resource.users=[{...}]
) лучше не использовать query‑параметры, а реализовывать отдельный метод поиска с POST‑телом.Неизвестные параметры — по умолчанию игнорируем.
Для полнотекстового поиска лучше использовать параметр
search
.
С продвинутой реализацией через параметр filter
можно ознакомиться по ссылкам:
Microsoft REST API Guidelines — filter: https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#filter
Google API Design Guide — Filtering: https://google.aip.dev/160
Сортировка
Простой вариант — не парсить единый orderby
, а держать сортировки в явных переменных.
created_at_sort=asc|desc
, title_sort=asc|desc
. Отсутствие — поле не участвует.
Примеры:
GET /v1/resources?created_at_sort=asc
GET /v1/resources?created_at_sort=asc&title_sort=desc
Сортировка через один query‑параметр представлена по ссылкам:
Microsoft REST API Guidelines — orderby: https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#orderby
Google API Design Guide — List: https://google.aip.dev/132
ИМХО: использование
orderby
/filter
— это парсинг и связка с внутренней моделью, что делать в каждом микросервисе накладно; явные переменные делают контракт очевидным и упрощают реализацию.
Мягкое удаление (soft delete)
Оставляем единую схему: статус удалённости храним в колонке, а не в новых таблицах.
Модель: добавляем поле deleted_at: string|null
.
Удалить (в архив) —
DELETE /v1/resources/{id}
→ 204 (проставляемdeleted_at
).Восстановить —
PATCH /v1/resources/{id}/restore
→ 200 (ставимdeleted_at = null
и возвращаем ресурс).Список с удалёнными —
GET /v1/resources?include_deleted=true
.
Ответ (пример):
{
"resources": [
{ "id": "guid", "title": "...", "deleted_at": null },
{ "id": "guid", "title": "...", "deleted_at": "2024-11-12T10:36:15Z" }
],
"total": 100
}
Полное удаление (подтверждаем явно) —
DELETE /v1/resources/{id}?force=true
→ 204. Полезно для «удалить из архива/навсегда».
Частичное извлечение полей (fields)
Простого решения нет: нужна разборка строки и динамическая сборка ответа. Если это действительно нужно — вы делаете что-то сложнее CRUD и стоит опираться на гайдлайны:
Microsoft REST API Guidelines — select query: https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#query-options
Google API Design Guide — Partial responses: https://google.aip.dev/157#field-masks-parameter
Примеры:
GET /v1/resources?fields=id,title
GET /v1/resources/{id}?fields=id,status
Ответ (для списка):
{
"resources": [
{ "id": "guid", "title": "..." }
],
"total": 123
}
Примеры «всё вместе»
Список с пагинацией, фильтрами, сортировкой и усечёнными полями
GET /v1/resources?skip=10&take=5&status_in=active,draft&created_at_sort=desc&fields=id,title&include_deleted=false
{
"resources": [
{ "id": "guid1", "title": "Design Guide" },
{ "id": "guid2", "title": "API Patterns" }
],
"total": 100
}
Мягкое удаление / восстановление / форс‑удаление
DELETE /v1/resources/{id} → 204
PATCH /v1/resources/{id}/restore → 200 (тело)
DELETE /v1/resources/{id}?force=true → 204
Часть 4. Пользовательские методы и пакетные операции
Стандартных GET/POST/PUT/PATCH/DELETE
хватает на ~80% CRUD. Оставшиеся ~20% — доменные действия, которые:
меняют состояние ресурса не как «полная/частичная замена» (пример: «подтвердить», «отменить», «применить скидку»);
запускают вычисление (пример: «посчитать тариф»);
не создают новый ресурс напрямую, но производят эффект (пример: «просмотреть ресурс»).

Правила
Глагол после
{id}
:POST /v1/resources/{id}/apply-discount
.Метод — чаще POST (есть побочный эффект).
Исключения: вычислительные методы и счётчики, которые не меняют состояние → GET.
Примеры пользовательских методов
Просмотр ресурса (есть побочный эффект записи в лог/метрику)
POST /v1/resources/{id}/view
Счётчики (без побочного эффекта)
Публичные:
GET /v1/resources/count
GET /v1/users/{user-id}/resources/count
Внутренние (на нескольких ресурсах):
GET /_internal/resources/count
GET /_internal/organizations/{organization-id}/resources/count
GET /_internal/organizations/{organization-id}/users/{user-id}/resources/count
Расчёт стоимости (вычисление, без изменения состояния)
POST /v1/resources/calculate
Тело:
{ "destination": "New York", "deliverySpeed": "express" }
Ответ 200:
{ "price": 42.50, "currency": "USD", "eta_days": 2 }
Применение скидки (мутация)
POST /v1/resources/{id}/apply-discount
Тело:
{ "discount_сode": "SUMMER21" }
Пакетные операции (batch)
Снижают количество запросов к API, позволяют согласованно менять несколько ресурсов и упрощают клиентскую логику.
Общие принципы
Однотипность внутри запроса: не смешиваем «создать» и «удалить».
Атомарность: либо всё, либо ничего.
Лимиты: ограничиваем количество элементов и возвращаем понятную ошибку 413 Payload Too Large с подсказкой.
Массовое чтение
Частый анти‑паттерн:
GET /v1/resources/batch?ids=1&ids=2&...
Риск — лимит длины URL при больших батчах. Лучше так:
POST /v1/resources/batch-get
Тело:
{ "ids": ["guid-1", "guid-2"] }
Ответ 200:
{
"resources": [
{ "id": "guid-1", "title": "A", "status": "draft" },
{ "id": "guid-2", "title": "B", "status": "active" }
]
}
Массив без обёртки ограничивает нас — формат с объектом позволяет добавить флаги (
idempotency
,force
и др.).
Массовое создание
POST /v1/resources/batch
Тело:
{
"resources": [
{ "title": "A", "status": "draft" },
{ "title": "B", "status": "active" }
]
}
Ответ 201:
{
"resources": [
{ "id": "guid-1", "title": "A", "status": "draft" },
{ "id": "guid-2", "title": "B", "status": "active" }
]
}
Batch‑обновление
PUT /v1/resources/batch
Тело:
{
"resources": [
{ "id": "guid-1", "status": "active", "title": "API Patterns" },
{ "id": "guid-2", "status": "active", "title": "Design Guide" }
]
}
Batch‑удаление
POST /v1/resources/batch-delete
Тело:
{
"ids": ["guid-1", "guid-2"],
"force": false
}
Ответ 200.
Согласно RFC 9110 рекомендуется использовать POST с телом, вместо DELETE
.
Часть 5. Критика

Стоит ли на POST/PUT/PATCH всегда возвращать весь ресурс, если сервер уже принял изменения и на клиенте есть актуальная версия?
Да. В обычных CRUD‑сервисах это упрощает жизнь: меньше дополнительных запросов, легче дебажить и тестировать.
Не упрёмся ли мы в лимиты по количеству/длине query‑параметров? Это не станет проблемой для фильтрации и сортировки?
Для типичного CRUD с десятком явных фильтров проблем нет. Когда критериев много — используем метод POST и передаём параметры в теле.
Заключение
Я намеренно не касался в статье тем идемпотентности, ETag, версионирования и других аспектов — о них написано достаточно в официальных гайдлайнах. Цель была показать ход мыслей и что 80% задач можно закрыть небольшим и понятным набором правил. Придерживаясь их, вы снижаете риски дублирования, облегчаете поддержку и оставляете простор для расширения. Остальные 20% случаев потребуют осознанных решений — и здесь полезно сверяться с индустриальными стандартами.
Если у вас есть рекомендации, как сделать эту статью ещё практичнее и доступнее — напишите в комментариях или мне в профиль. По мере накопления фидбэка дополню разделы.
Полезные ссылки
RFC 9110 — HTTP Semantics (методы, коды, заголовки)
https://datatracker.ietf.org/doc/html/rfc9110RFC 5789 — метод PATCH
https://datatracker.ietf.org/doc/html/rfc5789Google API Design Guide
https://cloud.google.com/apis/design/Microsoft REST API Guidelines
https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md
Комментарии (0)
NightBlade74
17.09.2025 13:14Опять притащили коды статусов HTTP как коды ответов на запросы к уровню приложения.
HTTP всегда должен отвечать 2хx, если запрос с точки зрения HTTP выполнен, 4хх - если ошибка со стороны клиента, 5хх - что-то не так на сервере.
А вот в теле ответа быть уже статус ответа на сам REST запрос, причем желательно не просто одним числом, а поподробнее.
А у вас программист, вызывающий сервис, потом голову будет ломать, что же он перепутал: эндпоинт или фильтр запроса и с многоэтажными матами пытаться парсить строки ответов, следующие за HTTP-статусами в заголовке. Не надо смешивать уровни OSI.
ArchDemon
Какой-то странный REST у нас получается. Запрос - явный GET, но из-за длинных параметров (и невозможности передать параметры в теле) мы должны использовать POST.
Тогда проще использовать POST для всего. И получим RPC (JSON-RPC)
id_bones Автор
Спасибо за комментарий! Цель статьи обозначить вектор проектирования, конкретная реализация в разных компаниях и командах может отличаться, всё зависит от контекста.
Использование POST для всего подряд усложняет настройки кэширования, ссылки/закладки и другие оптимизации опирающиеся на rest стандарты