Як правильно іменувати 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/ua
✅ Добре: /api/v1/users?status=ACTIVE&country=UA
Правило вибору:
- 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 та ускладнення клієнтської інтеграції.
- Необхідність валідації всього ланцюжка зовнішніх ключів (Foreign Keys) у 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 у нижньому регістрі).