Chuyển đến nội dung chính

レッスン 16: OpenAPI と HATEOAS を使用した API ドキュメント

SpringDoc OpenAPI 3 — Swagger UI および API ドキュメントを自動的に生成します。 HATEOAS とハイパーメディア駆動の API。 API のバージョン管理戦略とベスト プラクティス。

💻 プログラミング — レッスン 15 レッスン 16: OpenAPI を使用した API ドキュメントと ハテオアス

Spring Boot 4: 基本から上級まで

パート 4: 高度な機能

xdev.asia

はじめに

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/) を使用する必要があります。

演習

  1. SpringDoc OpenAPI 統合: すべてのコントローラーに @Operation、@ApiResponse のアノテーションを付け、パブリック API と管理 API 用のグループ化されたドキュメントを作成します。
  2. 製品 API に HATEOAS を実装します。各製品応答にはセルフ リンク、コレクション リンク、関連リソース リンクが含まれます。
  3. API バージョン管理を作成します: ProductController の /api/v1 および /api/v2、v2 にはフィールドが追加され、応答形式が変更されました