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

Lesson 16: API Documentation with OpenAPI & HATEOAS

SpringDoc OpenAPI 3 — automatically generates Swagger UI and API docs. HATEOAS and hypermedia-driven APIs. API versioning strategies and best practices.

💻 Programming — Lesson 15 Lesson 16: API Documentation with OpenAPI & HATEOAS

Spring Boot 4: From Basics to Advanced

Part 4: Advanced Features

xdev.asia

Introduction

API documentation is an indispensable part when building a REST API. SpringDoc OpenAPI automatically generates API docs from code, while HATEOAS helps APIs self-describe through hypermedia links.


1. SpringDoc OpenAPI

1.1 Setup

// 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 Configure OpenAPI metadata

@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 Annotate Controller

@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 Group APIs

@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();
}

Access:

  • Swagger UI: http://localhost:8080/swagger-ui.html
  • JSON docs: http://localhost:8080/api-docs

2. HATEOAS — Hypermedia As The Engine Of Application State

2.1 Setup

// build.gradle.kts
implementation("org.springframework.boot:spring-boot-starter-hateoas")

2.2 Create Resource Model

public class ProductModel extends RepresentationModel<ProductModel> {
    private Long id;
    private String name;
    private BigDecimal price;
    private String category;

    // constructors, getters
}

2.3 Add Links to Response

@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 Response in HAL+JSON format

{
  "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 Versioning Strategies

StrategyExampleProsCons
URL Path/api/v1/productsClear, easy to cacheChange URL
Query Param/api/products?version=1FlexibleEasy to forget
HeadersAccept: application/vnd.api.v1+jsonClean URLDifficult to test using browser
Media TypeContent-Type: application/vnd.api.v1+jsonMost RESTfulComplex

Recommended: URL Path versioning (/api/v1/) is the simplest and most popular.


Summary

  • SpringDoc OpenAPI automatically generates Swagger UI and JSON docs from controller annotations
  • HATEOAS adds hypermedia links to the response, helping clients explore the API without needing documentation
  • API versioning should use URL path (/api/v1/) for most cases

Exercises

  1. SpringDoc OpenAPI integration: annotate all controllers with @Operation, @ApiResponse, create grouped docs for public and admin APIs
  2. Implement HATEOAS for Product API: each product response contains self link, collection link, and related resources links
  3. Create API versioning: /api/v1 and /api/v2 for ProductController, v2 has additional fields and changed response format