Проектирование эндпоинтов: именование, версионирование, вложенность
Хороший URL, это контракт. Он должен быть понятен без документации, предсказуем и устойчив к изменениям. Плохой URL заставляет команду держать в голове тонны неочевидных правил.
Именование ресурсов
Правило 1: Существительные, не глаголы
URL описывает ресурс, а не действие. Действие выражается HTTP-методом.
✅ GET /api/v1/orders
✅ POST /api/v1/orders
✅ DELETE /api/v1/orders/42
❌ GET /api/v1/getOrders
❌ POST /api/v1/createOrder
❌ POST /api/v1/deleteOrder?id=42
Антипаттерн с глаголами, это RPC поверх HTTP. Если ты пишешь /getUser или /sendEmail в URL, это не REST.
Правило 2: Множественное число
Ресурсы, коллекции. Используй множественное число всегда.
✅ /users ✅ /products ✅ /orders
❌ /user ❌ /product
Исключение, синглтоны (ресурс существует в одном экземпляре):
/users/42/profile → профиль конкретного пользователя (один на пользователя)
Правило 3: lowercase и дефисы
✅ /api/v1/product-categories
✅ /api/v1/user-profiles
❌ /api/v1/productCategories (camelCase в URL)
❌ /api/v1/product_categories (underscore)
Правило 4: Никаких расширений файлов
✅ GET /api/v1/reports/42
Accept: application/pdf ← формат через заголовок
❌ /api/v1/reports/42.pdf
Вложенность (Nested Resources)
Вложенные ресурсы отражают отношения «один ко многим»:
/users/42/orders → заказы пользователя 42
/orders/17/items → позиции заказа 17
/categories/5/products → товары в категории 5
Когда вкладывать
- Вложенный ресурс не имеет смысла без родителя,
/orders/17/items - Нужно явно выразить владение,
/users/42/ordersvs/orders?user_id=42
Когда НЕ вкладывать
❌ /users/42/orders/17/items/3/product/category
Глубже 2 уровней, почти всегда проблема в дизайне. Правило: не более 2 уровней вложенности.
Альтернатива, плоские URL с фильтрами:
✅ /order-items?order_id=17
✅ /products?category_id=5
Версионирование API
Версионирование, это страховка: ты можешь менять API, не ломая существующих клиентов.
Стратегия 1: Версия в URL (рекомендуется)
https://api.example.com/v1/users
https://api.example.com/v2/users
Используют Stripe, GitHub, Twilio, 90% крупных публичных API. Почему: очевидно в URL, легко тестировать в браузере, хорошо кэшируется.
Стратегия 2: Версия в заголовке
GET /users/42
Accept: application/vnd.myapi.v2+json
Теоретически чище (URL идентифицирует ресурс, не версию), но неудобно тестировать и хуже кэшируется.
Стратегия 3: Версия в query parameter
GET /users/42?version=2
Используется реже. Проблема: query params семантически для фильтрации данных, а не для роутинга.
Вывод: для публичных API, версия в URL. Это самый понятный вариант для потребителей.
Действия которые не вписываются в CRUD
Иногда нужно выразить действие которое не является простым созданием/чтением/изменением/удалением.
Паттерн 1: Вложенный глагол-ресурс
POST /orders/17/cancel → отменить заказ
POST /users/42/activate → активировать аккаунт
POST /payments/99/refund → вернуть деньги
cancel, activate, refund, «действия» оформленные как ресурсы. POST сигнализирует о создании события.
Паттерн 2: Изменение состояния через PATCH
PATCH /orders/17
{ "status": "cancelled" }
Семантически чисто, меняем поле status. Хорошо для конечных автоматов.
Паттерн 3: Отдельный ресурс для события
POST /order-cancellations → создаём факт отмены как отдельную сущность
POST /refunds → создаём возврат
Хорошо когда событие само по себе важная сущность, нужно хранить историю.
Частые ошибки проектирования из реальной практики
Ошибка 1: Глагол в URL
❌ POST /api/sendEmail
✅ POST /api/emails (создаём письмо, система его отправляет)
Ошибка 2: Действие в GET-запросе
❌ GET /api/users/42/delete (GET не должен менять данные)
✅ DELETE /api/users/42
Ошибка 3: Нет версионирования с первого дня
Добавить /v1/ проще сразу, чем мигрировать сотни клиентов потом.
Ошибка 4: Непоследовательность
❌ /api/v1/users/42 (ID в пути)
❌ /api/v1/products?id=5 (ID в query param)
✅ /api/v1/users/42
✅ /api/v1/products/5
Итоговые правила
| Правило | Плохо | Хорошо |
|---|---|---|
| Существительные | /getUser |
/users |
| Множественное число | /user/42 |
/users/42 |
| lowercase + дефис | /userProfiles |
/user-profiles |
| Глубина вложенности | /a/1/b/2/c/3 |
/a/1/b/2 |
| Версия с первого дня | /users |
/v1/users |
| Формат не в URL | /report.pdf |
/report + Accept header |