Когда разработчик впервые видит ваш API, он читает URL. Хорошо выстроенный путь говорит сам за себя: GET /orders/{id}/items — понятно без документации. Плохой путь (/getOrderItemList?orderId=5) заставляет лезть в Swagger каждый раз.
В этой статье разберём, как правильно строить URL: как называть ресурсы, как выбирать HTTP-методы и насколько глубокой может быть вложенность.
Четыре цели хорошего URL
Прежде чем переходить к правилам, стоит понять, что именно мы хотим получить.
Предсказуемость — разработчик, знающий один эндпоинт, угадывает остальные. Если есть GET /orders, то логично предположить GET /orders/{id} и POST /orders.
Единообразие — одинаковые правила везде. Не /user-items в одном месте и /orderItems в другом.
Читаемость — URL читается как предложение: GET /orders/{id}/items — «получить позиции заказа».
Стабильность — URL — это публичный контракт. Его изменение ломает чужой код. Менять URL — то же самое, что менять сигнатуру публичного метода библиотеки.
Как записывать пути
Есть несколько простых правил, которых нужно придерживаться всегда.
Только строчные буквы и дефис между словами (это называется kebab-case):
/order-items ✓
/delivery-addresses ✓
/OrderItems ✗ заглавные буквы
/order_items ✗ подчёркивание (snake_case)
/deliveryAddresses ✗ верблюжий регистр (camelCase)
Без слеша в конце, без расширений в имени файла:
/orders ✓
/orders/ ✗ слеш в конце лишний
/orders.json ✗ формат указывают через заголовок Accept
Без глаголов в пути — для действий уже есть HTTP-методы:
GET /orders ✓ метод GET = получить
POST /orders ✓ метод POST = создать
GET /getOrders ✗ глагол в URL лишний
POST /createOrder ✗ глагол в URL лишний
Служебные эндпоинты — вне основного API
Большинство API строится на базовом пути /api/v1/.... Но есть служебные эндпоинты, которые нужны инфраструктуре, — они живут отдельно:
/health— работает ли приложение./ready— готово ли оно принимать трафик (используется в Kubernetes)./info— версия и метаинформация./metrics— метрики Prometheus / Micrometer.
Их не версионируют и не требуют авторизации пользователя (или защищают через отдельный порт управления).
Коллекции и одиночные ресурсы
Одна из самых частых ошибок — путаница с числом существительного в пути.
Коллекция (список объектов) — множественное число:
/orders список заказов
/orders/{id} конкретный заказ
/users список пользователей
/users/{id} конкретный пользователь
Singleton (ресурс, который существует в единственном числе для данного контекста) — единственное число:
/users/{id}/profile профиль пользователя (у каждого один)
/settings глобальные настройки (одни для всей системы)
Типичная ошибка — смешивать числа:
/order ✗ используйте /orders
/orders/{id}/item ✗ используйте /orders/{id}/items
Имена берут из доменного языка
Ресурс должен называться так, как называется понятие в вашем проекте. Если в коде объект называется Order, то путь — /orders, а не /purchases или /transactions. Это упрощает навигацию по коду и документации — везде одно и то же слово.
Примеры соответствия:
Order→/ordersOrderItem→/orders/{id}/itemsDeliveryAddress→/delivery-addressesPayment→/payments
HTTP-методы: какой когда использовать
HTTP предоставляет несколько методов, каждый из которых несёт смысловую нагрузку. Нарушение этого смысла приводит к неожиданному поведению.
| Метод | Назначение | Повторный вызов безопасен? | Типичный статус |
|---|---|---|---|
GET | Чтение данных | Да | 200 |
POST | Создание / команда | Нет | 201 + заголовок Location |
PUT | Полная замена ресурса | Да | 200 |
PATCH | Частичное обновление | Да (на практике) | 200 |
DELETE | Удаление | Да | 204 |
Важное правило: GET не должен менять данные. Это самая опасная ошибка — если операция имеет побочный эффект (списание денег, отмена заказа), она идёт через POST.
// Отмена заказа — это команда с побочным эффектом
@PostMapping("/orders/{id}/cancel")
public OrderResponse cancel(@PathVariable Long id) { ... }
// Так нельзя: GET подразумевает безопасное чтение
@GetMapping("/orders/{id}/cancel")
public OrderResponse cancel(@PathVariable Long id) { ... }
POST также используется для команд, которые не создают новый ресурс, но делают что-то необратимое: POST /orders/{id}/confirm, POST /payments/{id}/refund.
Вложенность: не глубже двух уровней
REST допускает вложенные пути, но злоупотреблять этим не стоит.
До двух уровней — нормально:
/orders/{id} 1 уровень
/orders/{id}/items 2 уровня
/orders/{id}/items/{id} 2 уровня + идентификатор
Три уровня и глубже — уже сложно:
/users/{id}/orders/{id}/items/{id} ✗ слишком глубоко
Когда хочется сделать три уровня, обычно лучше перейти к плоскому пути с фильтром:
/items?orderId={id} ✓ вместо /orders/{id}/items
Вложенность оправдана, когда дочерний ресурс не существует без родителя. Например, позиция заказа (OrderItem) без заказа (Order) не имеет смысла — значит, /orders/{id}/items логично. Если же ресурс может существовать самостоятельно, лучше flat + filter.
Идентификатор — всегда в пути, не в теле
Если эндпоинт работает с конкретным объектом, его идентификатор должен быть в URL, а не в теле запроса:
PUT /orders/{id} ✓
PUT /orders { "id": 5, ... } ✗ id в теле запроса
В самом дизайне URL path-переменная для идентификатора всегда записывается как {id} — контекст даёт имя ресурса в предыдущем сегменте:
/orders/{id} первый {id} = идентификатор заказа
/orders/{id}/items/{id} второй {id} = идентификатор позиции
Коротко
- URL — публичный контракт; его изменение ломает клиентский код.
- Пишите пути строчными буквами через дефис (kebab-case):
/order-items, а не/orderItemsили/order_items. - Без слеша в конце, без расширений (
.json) в пути. - Без глаголов в URL — для действий есть HTTP-методы.
- Коллекции — множественное число (
/orders); singleton — единственное (/profile). - Имена ресурсов берут из доменного языка проекта.
- GET — только чтение; POST — создание и команды с побочным эффектом.
- Максимум два уровня вложенности; глубже — плоский путь с фильтром (
?orderId=...). - Идентификатор — в пути, не в теле запроса.
- Служебные эндпоинты (
/health,/ready,/metrics) — вне основного/api/v1/.
Что почитать дальше
- Версионирование REST API — как вводить
/v2и не ломать клиентов. - Query-параметры и пагинация — фильтры, сортировка, курсорная пагинация.
- Ошибки и RFC 9457 — стандартный формат ошибок.