Как правильно именовать REST endpoints
Именование REST эндпоинтов (URIs) формирует интуитивный и предсказуемый интерфейс вашей системы. В RESTful архитектуре URI идентифицирует ресурс (существительное), а действие на...
🟢 Junior Level
Именование REST эндпоинтов (URIs) формирует интуитивный и предсказуемый интерфейс вашей системы. В RESTful архитектуре URI идентифицирует ресурс (существительное), а действие над ним определяется HTTP-методом.
Базовые правила именования
- Используйте существительные, а не глаголы:
- ✅
GET /api/v1/users(получить пользователей) - ❌
GET /api/v1/getUsersилиPOST /api/v1/createUser
- ✅
- Используйте множественное число для коллекций:
- Коллекция представляет набор объектов:
/users,/orders,/products. - Конкретный элемент коллекции адресуется по ID:
/users/42,/orders/105. - ❌ Использование единственного числа (
/user/42) создает путаницу при обращении к списку.
- Коллекция представляет набор объектов:
- Используйте
kebab-caseи нижний регистр (RFC 3986):- URL чувствительны к регистру на большинстве серверов (Linux, NGINX).
- ✅
/user-profiles,/audit-logs - ❌
/user_profiles(snake_case) или/userProfiles(camelCase)
- Отображайте иерархию отношений (Sub-resources):
- Ресурс, принадлежащий другому ресурсу:
- ✅
GET /api/v1/users/42/orders(все заказы пользователя 42) - ✅
GET /api/v1/users/42/orders/10(заказ 10 пользователя 42)
🟡 Middle Level
Разделение ответственности: Path Variable vs Query Parameter
Частая ошибка проектирования — помещение параметров поиска и фильтрации прямо в иерархию URI:
❌ Плохо: /api/v1/users/status/active/country/de
✅ Хорошо: /api/v1/users?status=ACTIVE&country=DE
Правило выбора:
- Path Variable (
@PathVariable): Используется строго для уникальной идентификации ресурса или его владельца в иерархии (/users/{id},/departments/{deptId}/employees). - Query Parameter (
@RequestParam): Используется для фильтрации, сортировки, пагинации и поиска (?status=ACTIVE&sort=createdAt,desc&page=0&size=20).
Как проектировать бизнес-действия, не укладывающиеся в CRUD?
В реальных приложениях часто возникают действия, которые сложно представить простым обновлением: «отправить на модерацию», «отменить заказ», «заблокировать пользователя».
Существует 3 общепринятых REST-паттерна:
1. Модификация состояния через PATCH (Рекомендуемый для простых состояний)
PATCH /api/v1/orders/100 HTTP/1.1
Content-Type: application/json
{
"status": "CANCELLED",
"reason": "Customer request"
}
2. Создание подчиненного ресурса-действия (Action Sub-resource)
Действие рассматривается как доменное событие или факт создания бизнес-записи:
POST /api/v1/orders/100/cancellations HTTP/1.1
POST /api/v1/users/42/password-resets HTTP/1.1
3. Контроллер команд (RPC-style подресурс через двоеточие или глагол)
Допустимо в сложных сценариях по рекомендациям Google Cloud API Design Guide:
POST /api/v1/orders/100:cancel HTTP/1.1
POST /api/v1/documents/50:publish HTTP/1.1
Проблема хвостового слэша (Trailing Slash)
По спецификации RFC 3986 адреса /users и /users/ являются двумя разными URI.
В Spring Boot 3 обработка TrailingSlashMatch по умолчанию отключена для исключения дублирования маршрутов и уязвимостей безопасности. Запрос /users/ при настроенном /users вернет 404 Not Found (или 308 Permanent Redirect).
🔴 Senior Level
Глубина вложенности и антипаттерн “URI Deep Nesting”
Создание глубоких цепочек ресурсов считается серьезным антипаттерном:
❌ /api/v1/companies/1/departments/5/teams/12/projects/99/tasks/1001
Проблемы глубокой вложенности:
- Раздувание длины URL и усложнение интеграции.
- Необходимость валидации всей цепочки внешних ключей в SQL-запросах.
- Проблема авторизации: нужно проверить права на компанию 1, отдел 5, команду 12 и проект 99.
Senior-решение: Плоский дизайн (Flat Resource URLs) Если идентификатор дочернего ресурса глобально уникален (UUID или уникальный Primary Key), держите вложенность не глубже 2 уровней:
✅ GET /api/v1/projects/99/tasks (список задач конкретного проекта)
✅ GET /api/v1/tasks/1001 (доступ к конкретной задаче напрямую)
✅ PUT /api/v1/tasks/1001 (обновление задачи напрямую)
Защита от IDOR через псевдо-ресурс /me
В веб- и мобильных приложениях передача ID авторизованного пользователя в URL создает уязвимость IDOR (Insecure Direct Object Reference), если разработчик забыл проверить соответствие переданного {userId} текущему JWT токену.
Архитектурное решение: Для данных текущего авторизованного пользователя используйте ресурс /me:
GET /api/v1/users/me
GET /api/v1/users/me/orders
GET /api/v1/users/me/cart
Сервер извлекает идентификатор пользователя напрямую из контекста безопасности (SecurityContextHolder), исключая возможность подмены ID.
Реализация RESTful контроллера в Spring Boot 3
package com.example.controller;
import com.example.dto.OrderResponseDto;
import com.example.dto.UserResponseDto;
import com.example.service.OrderService;
import com.example.service.UserService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1")
public class ApiController {
private final UserService userService;
private final OrderService orderService;
public ApiController(UserService userService, OrderService orderService) {
this.userService = userService;
this.orderService = orderService;
}
// 1. Безопасное получение профиля текущего пользователя (/me)
@GetMapping("/users/me")
public ResponseEntity<UserResponseDto> getCurrentUser(@AuthenticationPrincipal Jwt jwt) {
Long currentUserId = Long.valueOf(jwt.getSubject());
return ResponseEntity.ok(userService.findById(currentUserId));
}
// 2. Вложенный ресурс: получение заказов конкретного пользователя
@GetMapping("/users/{userId}/orders")
public ResponseEntity<Page<OrderResponseDto>> getUserOrders(
@PathVariable Long userId,
@RequestParam(required = false, defaultValue = "COMPLETED") String status,
Pageable pageable) {
return ResponseEntity.ok(orderService.findOrdersByUserId(userId, status, pageable));
}
// 3. Плоский доступ к конкретному заказу (глубина = 1 уровень)
@GetMapping("/orders/{orderId}")
public ResponseEntity<OrderResponseDto> getOrderById(@PathVariable Long orderId) {
return ResponseEntity.ok(orderService.findById(orderId));
}
}
Стандарт RFC 9110: Ошибка 405 Method Not Allowed
Если ресурс /api/v1/users/42 поддерживает только GET, PUT, DELETE, а клиент отправил POST /api/v1/users/42:
- Сервер не имеет права возвращать
404 Not Found. - Сервер обязан вернуть
405 Method Not Allowedс заголовком:HTTP/1.1 405 Method Not Allowed Allow: GET, PUT, DELETE, OPTIONS
🎯 Шпаргалка для интервью
30-секундный ответ
- Существительные во множественном числе:
/users,/orders,/products. Ресурс — сущность, действие — HTTP-метод.- Kebab-case и lowercase: Исключительно строчные буквы с дефисами (
/user-profiles).- Иерархия ресурсов:
/users/{userId}/orders, но глубина вложенности не должна превышать 2 уровней (для дочерних ресурсов с уникальным ID используется плоский URL:/orders/{orderId}).- Query-параметры: Фильтрация, пагинация и сортировка передаются через query (
/orders?status=NEW&page=0), а не через сегменты пути.- Защита от IDOR: Использование эндпоинта
/me(/users/me/settings) для ресурсов текущего пользователя.- Если ресурс существует, но вызван неподдерживаемый HTTP-метод — возвращается 405 Method Not Allowed с обязательным заголовком
Allow.
4 каверзных вопроса с ответами
-
Почему в именовании ресурсов используется множественное число (
/users), а не единственное (/user)? Ответ: Сегмент URI обозначает коллекцию объектов предметной области.GET /usersвозвращает всю коллекцию, аGET /users/42указывает на конкретный элемент этой коллекции. Использование множественного числа обеспечивает строгую ортогональность и единообразие схемы API. -
Как в REST API спроектировать эндпоинт для перевода денег со счета на счет без использования глагола
transferMoney? Ответ: Перевод денег оформляется как создание ресурса транзакции:POST /api/v1/transfersс телом запроса{"sourceAccountId": 1, "targetAccountId": 2, "amount": 1000}. Ответ:201 Createdс созданным объектом перевода и его статусом. -
Что делать, если иерархия сущностей насчитывает 4–5 уровней (компания -> филиал -> отдел -> команда -> задача)? Ответ: Применять паттерн плоских URL (Flat URLs). Вложенные эндпоинты оставлять только для создания и списков:
GET /teams/{teamId}/tasks, а для манипуляций с конкретной задачей обращаться напрямую через ее идентификатор:GET /tasks/{taskId},PUT /tasks/{taskId}. -
Что обязан вернуть сервер, если клиент отправил POST на эндпоинт, поддерживающий только GET? Ответ: Сервер обязан вернуть статус
405 Method Not Allowedи обязательно включить заголовок ответаAllowсо списком поддерживаемых методов (Allow: GET, HEAD, OPTIONS), согласно RFC 9110 §15.5.6. Возврат кода 404 в такой ситуации является грубой архитектурной ошибкой.
Красные флаги (чего категорически нельзя говорить)
- ❌ «В URL можно использовать глаголы, если это RPC или сложное действие» (в REST действие всегда задается HTTP-методом или моделируется через ресурс).
- ❌ «Если метод не поддерживается, контроллер должен возвращать 404 Not Found» (должен возвращаться 405 Method Not Allowed с заголовком Allow).
- ❌ «Параметры фильтрации лучше делать частью пути URI, например /users/active/admins» (фильтры должны быть Query Parameters).
- ❌ «Для URL можно свободно смешивать camelCase и snake_case» (стандартом для URI является kebab-case в нижнем регистре).