
Введение
Привет, Хабр! Каждый раз, создавая новый эндпоинт, я ловил себя на мысли: «А как назвать маршрут?». Казалось, что где-то есть законы и правила, которые помогают создавать 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,draftGET /v1/resources?title_eq=DesignGET /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=ascGET /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,titleGET /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} → 204PATCH /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/countGET /v1/users/{user-id}/resources/count
Внутренние (на нескольких ресурсах):
GET /_internal/resources/countGET /_internal/organizations/{organization-id}/resources/countGET /_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/rfc9110 -
RFC 5789 — метод PATCH
https://datatracker.ietf.org/doc/html/rfc5789 -
Google API Design Guide
https://cloud.google.com/apis/design/ -
Microsoft REST API Guidelines
https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md
ссылка на оригинал статьи https://habr.com/ru/articles/947410/