はじめに
REST API を構築する場合、API ドキュメントは不可欠な部分です。 SpringDoc OpenAPI はコードから API ドキュメントを自動的に生成し、HATEOAS はハイパーメディア リンクを通じて API の自己記述を支援します。
1. SpringDoc OpenAPI
1.1 セットアップ
// build.gradle.kts
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.0")
# application.yml
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: method
1.2 OpenAPI メタデータの構成
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("My App API")
.version("1.0.0")
.description("REST API documentation for My App")
.contact(new Contact()
.name("Dev Team")
.email("[email protected]"))
.license(new License()
.name("MIT")
.url("https://opensource.org/licenses/MIT")))
.addSecurityItem(new SecurityRequirement()
.addList("Bearer Authentication"))
.components(new Components()
.addSecuritySchemes("Bearer Authentication",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.bearerFormat("JWT")
.scheme("bearer")));
}
}
1.3 コントローラーに注釈を付ける
@RestController
@RequestMapping("/api/v1/products")
@Tag(name = "Products", description = "Product management APIs")
public class ProductController {
@Operation(
summary = "Get product by ID",
description = "Returns a single product by its ID"
)
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Product found",
content = @Content(schema =
@Schema(implementation = ProductResponse.class))),
@ApiResponse(responseCode = "404",
description = "Product not found")
})
@GetMapping("/{id}")
public ResponseEntity<ProductResponse> getProduct(
@Parameter(description = "Product ID", example = "1")
@PathVariable Long id) {
return ResponseEntity.ok(productService.getProduct(id));
}
@Operation(summary = "Search products with filters")
@GetMapping
public ResponseEntity<Page<ProductResponse>> searchProducts(
@Parameter(description = "Search keyword")
@RequestParam(required = false) String keyword,
@Parameter(description = "Min price")
@RequestParam(required = false) BigDecimal minPrice,
@ParameterObject Pageable pageable) {
return ResponseEntity.ok(
productService.search(keyword, minPrice, pageable));
}
}
1.4 グループ API
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("public")
.pathsToMatch("/api/v1/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/api/admin/**")
.addOpenApiMethodFilter(method ->
method.isAnnotationPresent(PreAuthorize.class))
.build();
}
アクセス:
- Swagger UI:
http://localhost:8080/swagger-ui.html - JSON ドキュメント:
http://localhost:8080/api-docs
2. HATEOAS — アプリケーション状態のエンジンとしてのハイパーメディア
2.1 セットアップ
// build.gradle.kts
implementation("org.springframework.boot:spring-boot-starter-hateoas")
2.2 リソースモデルの作成
public class ProductModel extends RepresentationModel<ProductModel> {
private Long id;
private String name;
private BigDecimal price;
private String category;
// constructors, getters
}
2.3 応答へのリンクの追加
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
@GetMapping("/{id}")
public EntityModel<ProductResponse> getProduct(@PathVariable Long id) {
ProductResponse product = productService.getProduct(id);
return EntityModel.of(product,
linkTo(methodOn(ProductController.class).getProduct(id))
.withSelfRel(),
linkTo(methodOn(ProductController.class).getAllProducts(Pageable.unpaged()))
.withRel("products"),
linkTo(methodOn(ReviewController.class).getReviews(id))
.withRel("reviews")
);
}
@GetMapping
public CollectionModel<EntityModel<ProductResponse>> getAllProducts(
Pageable pageable) {
Page<ProductResponse> products = productService.getAll(pageable);
List<EntityModel<ProductResponse>> productModels = products.stream()
.map(product -> EntityModel.of(product,
linkTo(methodOn(ProductController.class)
.getProduct(product.id())).withSelfRel()))
.toList();
return CollectionModel.of(productModels,
linkTo(methodOn(ProductController.class)
.getAllProducts(pageable)).withSelfRel());
}
}
2.4 HAL+JSON 形式の応答
{
"id": 1,
"name": "Spring Boot in Action",
"price": 450000,
"_links": {
"self": {
"href": "http://localhost:8080/api/v1/products/1"
},
"products": {
"href": "http://localhost:8080/api/v1/products"
},
"reviews": {
"href": "http://localhost:8080/api/v1/products/1/reviews"
}
}
}
2.5 RepresentationModelAssembler
@Component
public class ProductModelAssembler
implements RepresentationModelAssembler<Product, EntityModel<ProductResponse>> {
@Override
public EntityModel<ProductResponse> toModel(Product entity) {
ProductResponse response = ProductResponse.from(entity);
return EntityModel.of(response,
linkTo(methodOn(ProductController.class)
.getProduct(entity.getId())).withSelfRel(),
linkTo(methodOn(ProductController.class)
.getAllProducts(Pageable.unpaged())).withRel("products"));
}
}
3. API のバージョン管理戦略
| 戦略 | 例 | 長所 | 短所 |
|---|---|---|---|
| URL パス | /api/v1/製品 | クリアでキャッシュしやすい | URLを変更 |
| クエリパラメータ | /api/製品?バージョン=1 | 柔軟 | 忘れやすい |
| ヘッダー | 受け入れる: application/vnd.api.v1+json | クリーンな URL | ブラウザを使用したテストは難しい |
| メディアの種類 | コンテンツ タイプ: application/vnd.api.v1+json | 最も RESTful | 複雑な |
推奨: URL パスのバージョン管理 (/api/v1/) が最もシンプルで最も人気があります。
概要
- SpringDoc OpenAPI はコントローラーのアノテーションから Swagger UI と JSON ドキュメントを自動的に生成します
- HATEOAS はレスポンスにハイパーメディア リンクを追加し、クライアントがドキュメントを必要とせずに API を探索できるようにします。
- API のバージョン管理には、ほとんどの場合、URL パス (/api/v1/) を使用する必要があります。
演習
- SpringDoc OpenAPI 統合: すべてのコントローラーに @Operation、@ApiResponse のアノテーションを付け、パブリック API と管理 API 用のグループ化されたドキュメントを作成します。
- 製品 API に HATEOAS を実装します。各製品応答にはセルフ リンク、コレクション リンク、関連リソース リンクが含まれます。
- API バージョン管理を作成します: ProductController の /api/v1 および /api/v2、v2 にはフィールドが追加され、応答形式が変更されました