hateoas について
リンクでリソースや利用可能な操作を表す、ハイパーメディア API レスポンス構築向けの Spring HATEOAS ガイドです。Spring API 開発者を対象に、再利用可能なアセンブラーやページネーションリンクを扱います。
主な機能
- 01リンク付きリソース表現
- 02状態に応じたアクションリンク
- 03再利用可能な表現アセンブラー
- 04自動ページネーションリンク
- 05型安全なリンク生成
活用シーン
- 01API リソースへのナビゲーションリンク追加
- 02有効な操作だけの提示
- 03ナビゲーション付きページ分割コレクションの返却
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ rrezartprebreza/ spring-boot-skills --skill hateoasインストール後、Claude Code で/hateoasを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlexamples/bad-hateoas-controller.javagood-hateoas-controller.javatemplates/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リンクが必要です
コメント
Markdown 対応