← назад к разделу

Когда разработчик впервые видит ваш 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/orders
  • OrderItem/orders/{id}/items
  • DeliveryAddress/delivery-addresses
  • Payment/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 — стандартный формат ошибок.