Gymterview
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 для уменьшения количества запросов.