middle
Как проектировать REST API для связанных ресурсов?
Проектирование API для связанных ресурсов строится на выборе между вложенными URI, ссылками через идентификаторы и развёрнутыми ресурсами в зависимости от типа связи и потребностей клиента.
1. Вложенные ресурсы (Sub-resources)
Для отношения «один-ко-многим»:
Пример
GET /api/users/42/orders — все заказы пользователя 42
POST /api/users/42/orders — создать заказ для пользователя 42
GET /api/users/42/orders/7 — заказ 7 пользователя 42
DELETE /api/users/42/orders/7 — удалить заказ 7
2. Ссылки через идентификаторы
Пример
{
"id": 7,
"product": "Ноутбук",
"userId": 42,
"categoryId": 5
}
3. Развёрнутые (embedded) ресурсы
Включайте связанные данные по запросу для сокращения количества запросов:
Пример
GET /api/orders/7?expand=user,items
Пример
{
"id": 7,
"status": "DELIVERED",
"user": {"id": 42, "name": "Иван"},
"items": [{"id": 1, "product": "Ноутбук", "quantity": 1}]
}
4. Отношение многие-ко-многим
Используйте связующий ресурс:
Пример
GET /api/users/42/roles — роли пользователя
PUT /api/users/42/roles/3 — назначить роль 3
DELETE /api/users/42/roles/3 — убрать роль 3
5. Правила глубины вложенности
- Один уровень — хорошо:
/users/42/orders - Два уровня — допустимо:
/users/42/orders/7 - Три и более — избегайте. Вместо
/users/42/orders/7/items/3используйте/order-items/3
Реализация в Spring
@RestController
@RequestMapping("/api/users/{userId}/orders")
@RequiredArgsConstructor
public class UserOrderController {
private final OrderService orderService;
@GetMapping
public ResponseEntity<List<OrderDto>> getUserOrders(
@PathVariable Long userId,
@RequestParam(required = false) String status) {
List<OrderDto> orders = orderService.findByUserId(userId, status);
return ResponseEntity.ok(orders);
}
@PostMapping
public ResponseEntity<OrderDto> createOrder(
@PathVariable Long userId,
@Valid @RequestBody CreateOrderRequest request) {
OrderDto order = orderService.create(userId, request);
URI location = URI.create("/api/users/" + userId + "/orders/" + order.id());
return ResponseEntity.created(location).body(order);
}
@GetMapping("/{orderId}")
public ResponseEntity<OrderDto> getOrder(
@PathVariable Long userId,
@PathVariable Long orderId) {
OrderDto order = orderService.findByIdAndUserId(orderId, userId);
return ResponseEntity.ok(order);
}
}
Принцип: если ресурс имеет смысл только в контексте родительского — используйте вложенные URI. Если ресурс самостоятелен — выделяйте его на верхний уровень.
На собеседовании: нужно знать все подходы (вложенные URI, идентификаторы, expand) и правило глубины вложенности (не более 2-3 уровней). Частая ошибка — глубокая вложенность URI или неиспользование параметра expand для уменьшения количества запросов.