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
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL Path | /api/v1/products | Clear, easy to cache | Change URL |
| Query Param | /api/products?version=1 | Flexible | Easy to forget |
| Headers | Accept: application/vnd.api.v1+json | Clean URL | Difficult to test using browser |
| Media Type | Content-Type: application/vnd.api.v1+json | Most RESTful | Complex |
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
- SpringDoc OpenAPI integration: annotate all controllers with @Operation, @ApiResponse, create grouped docs for public and admin APIs
- Implement HATEOAS for Product API: each product response contains self link, collection link, and related resources links
- Create API versioning: /api/v1 and /api/v2 for ProductController, v2 has additional fields and changed response format