mcpField
H
hateoas

hateoas

github.com/rrezartprebreza開発32887 回インストール6 日前に更新
リポジトリ

hateoas について

リンクでリソースや利用可能な操作を表す、ハイパーメディア API レスポンス構築向けの Spring HATEOAS ガイドです。Spring API 開発者を対象に、再利用可能なアセンブラーやページネーションリンクを扱います。

主な機能

  1. 01リンク付きリソース表現
  2. 02状態に応じたアクションリンク
  3. 03再利用可能な表現アセンブラー
  4. 04自動ページネーションリンク
  5. 05型安全なリンク生成

活用シーン

  1. 01API リソースへのナビゲーションリンク追加
  2. 02有効な操作だけの提示
  3. 03ナビゲーション付きページ分割コレクションの返却

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rrezartprebreza/spring-boot-skills --skill hateoas

インストール後、Claude Code で/hateoasを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
examples/
bad-hateoas-controller.java
good-hateoas-controller.java
templates/
OrderModelAssembler.java

詳細

Spring HATEOAS

依存関係

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

リンクを追加するタイミング

  • self — すべてのリソースレスポンスに常に追加します
  • collection — リストエンドポイントへのリンクです
  • related resources — クライアントが頻繁に移動する必要がある場合に追加します
  • actions — 現在の状態で有効な状態遷移(例:cancel、ship)へのリンクです

リソースモデル

@Getter // Jackson serializes through getters — without them the body is only "_links"
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class OrderModel extends RepresentationModel<OrderModel> {
    private final UUID id;
    private final String status;
    private final String customerEmail;
    private final Instant createdAt;

    // Static factory with links
    public static OrderModel from(Order order) {
        OrderModel model = new OrderModel(
            order.getId(), order.getStatus().name(),
            order.getCustomerEmail(), order.getCreatedAt()
        );

        // Self link — always
        model.add(linkTo(methodOn(OrderController.class).getById(order.getId())).withSelfRel());

        // Collection link
        model.add(linkTo(methodOn(OrderController.class).list(null)).withRel("orders"));

        // Conditional action links based on state
        if (order.getStatus() == OrderStatus.PENDING) {
            model.add(linkTo(methodOn(OrderController.class)
                .cancelOrder(order.getId())).withRel("cancel"));
        }
        if (order.getStatus() == OrderStatus.PROCESSING) {
            model.add(linkTo(methodOn(OrderController.class)
                .shipOrder(order.getId())).withRel("ship"));
        }

        return model;
    }
}

コントローラー

@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    @GetMapping("/{id}")
    public ResponseEntity<OrderModel> getById(@PathVariable UUID id) {
        Order order = orderService.findById(id);
        return ResponseEntity.ok(OrderModel.from(order));
    }

    @GetMapping
    public ResponseEntity<CollectionModel<OrderModel>> list(Pageable pageable) {
        Page<Order> orders = orderService.findAll(pageable);

        List<OrderModel> models = orders.getContent().stream()
            .map(OrderModel::from)
            .toList();

        CollectionModel<OrderModel> collection = CollectionModel.of(models,
            linkTo(methodOn(OrderController.class).list(pageable)).withSelfRel()
        );

        // Pagination links
        if (orders.hasNext()) {
            collection.add(linkTo(methodOn(OrderController.class)
                .list(pageable.next())).withRel(IanaLinkRelations.NEXT));
        }
        if (orders.hasPrevious()) {
            collection.add(linkTo(methodOn(OrderController.class)
                .list(pageable.previousOrFirst())).withRel(IanaLinkRelations.PREV));
        }

        return ResponseEntity.ok(collection);
    }
}

レスポンス形式

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "PENDING",
  "customerEmail": "[email protected]",
  "_links": {
    "self": { "href": "http://api.example.com/api/v1/orders/550e8400" },
    "orders": { "href": "http://api.example.com/api/v1/orders" },
    "cancel": { "href": "http://api.example.com/api/v1/orders/550e8400/cancel" }
  }
}

RepresentationModelAssembler パターン

  • Spring が推奨する、エンティティから HATEOAS モデルを構築する方法です
  • RepresentationModelAssembler<Entity, Model> を実装します。コントローラー間で再利用できます
  • Model.from() を直接呼び出すのではなく、アセンブラーをコントローラーに注入します
@Component
public class OrderModelAssembler implements RepresentationModelAssembler<Order, EntityModel<OrderResponse>> {

    @Override
    public EntityModel<OrderResponse> toModel(Order order) {
        EntityModel<OrderResponse> model = EntityModel.of(OrderResponse.from(order),
            linkTo(methodOn(OrderController.class).getById(order.getId())).withSelfRel(),
            linkTo(methodOn(OrderController.class).list(null)).withRel("orders"));

        if (order.getStatus() == OrderStatus.PENDING) {
            model.add(linkTo(methodOn(OrderController.class)
                .cancelOrder(order.getId())).withRel("cancel"));
        }
        return model;
    }
}

ページネーションされたコレクション向けの PagedModel

  • PagedResourcesAssembler を使用すると、ページネーションリンク(first、prev、next、last)が自動的に追加されます
  • PagedResourcesAssembler<Order> をコントローラーに注入します。Spring が自動的に作成します
@GetMapping
public ResponseEntity<PagedModel<EntityModel<OrderResponse>>> list(
        Pageable pageable, PagedResourcesAssembler<Order> pagedAssembler) {
    Page<Order> orders = orderService.findAll(pageable);
    PagedModel<EntityModel<OrderResponse>> pagedModel =
        pagedAssembler.toModel(orders, orderModelAssembler);
    return ResponseEntity.ok(pagedModel);
}

注意点

  • エージェントが状態に関係なくすべてのリンクを追加します。有効なアクションに対してのみアクションリンクを追加してください
  • エージェントがリンクに URL をハードコードします。型安全なリンクには必ず linkTo(methodOn(...)) を使用してください
  • エージェントがプレーンな DTO を返します。EntityModel.of(dto, links...) でラップするか、RepresentationModel を拡張してください
  • エージェントがリンクのロジックをコントローラーに記述します。RepresentationModelAssembler に切り出してください
  • エージェントがページネーションリンクを手動で構築します。代わりに PagedResourcesAssembler を使用してください
  • エージェントが self リンクを忘れます。すべてのリソースに self リンクが必要です

コメント