Tài liệu API với OpenAPI và Swagger trong Spring Boot

Giới thiệu
Tài liệu API tốt là điểm khác biệt giữa việc developers yêu thích API của bạn và bỏ cuộc trong thất vọng. Với OpenAPI (trước đây là Swagger), bạn có thể tự động tạo tài liệu đẹp, tương tác trực tiếp từ code Spring Boot.
Bạn sẽ học được gì
✅ Các nguyên tắc cơ bản của OpenAPI 3.0
✅ Thiết lập SpringDoc OpenAPI trong Spring Boot
✅ Tự động tạo Swagger UI từ controllers
✅ Document endpoints với annotations phong phú
✅ Thêm ví dụ request/response và schemas
✅ Document authentication (JWT, OAuth2, API Keys)
✅ Tùy chỉnh giao diện Swagger UI
✅ Bảo mật API docs trong production
✅ Xuất OpenAPI JSON/YAML cho các công cụ bên ngoài
Yêu cầu
- Đã cài đặt Java 17+
- Kiến thức cơ bản Spring Boot (xem Hướng dẫn Bắt đầu)
- Quen thuộc với các khái niệm REST API (REST API Best Practices)
1. Hiểu về OpenAPI và Swagger
OpenAPI là gì?
OpenAPI Specification (OAS) là một tiêu chuẩn, giao diện không phụ thuộc ngôn ngữ để mô tả RESTful APIs. Nó cho phép cả con người và máy tính hiểu API làm gì mà không cần xem source code.
Lợi ích chính:
- Định dạng chuẩn hóa: JSON hoặc YAML mô tả toàn bộ API
- Tự động tạo: Tạo tài liệu, client SDKs, và server stubs
- Kiểm thử tương tác: Thử API calls trực tiếp trong trình duyệt
- Thiết kế contract-first: Định nghĩa API trước khi triển khai
Swagger là gì?
Swagger là hệ sinh thái công cụ xung quanh OpenAPI:
| Công cụ | Mục đích |
|---|---|
| Swagger UI | Tài liệu API tương tác trong trình duyệt |
| Swagger Editor | Thiết kế và chỉnh sửa OpenAPI specs |
| Swagger Codegen | Tạo code client/server từ specs |
| SwaggerHub | Nền tảng thiết kế API cộng tác |
Khuyến nghị: Luôn sử dụng OpenAPI 3.0+ cho các dự án mới.
2. Thiết lập SpringDoc OpenAPI
Thêm Dependencies
SpringDoc là thư viện hiện đại, được bảo trì tích cực cho OpenAPI trong Spring Boot.
Maven:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>Gradle:
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'Cấu hình cơ bản
Với zero configuration, SpringDoc tự động:
- Quét tất cả các class
@RestController - Tạo OpenAPI specification
- Phục vụ Swagger UI tại
/swagger-ui.html
Truy cập tài liệu:
http://localhost:8080/swagger-ui.html → Swagger UI tương tác
http://localhost:8080/v3/api-docs → OpenAPI JSON spec
http://localhost:8080/v3/api-docs.yaml → OpenAPI YAML specTùy chỉnh thông tin API
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("E-Commerce API")
.version("1.0.0")
.description("REST API để quản lý sản phẩm, đơn hàng và khách hàng")
.termsOfService("https://example.com/terms")
.contact(new Contact()
.name("Hỗ trợ API")
.email("[email protected]")
.url("https://example.com/support"))
.license(new License()
.name("MIT License")
.url("https://opensource.org/licenses/MIT")))
.addServersItem(new Server()
.url("https://api.example.com")
.description("Production server"))
.addServersItem(new Server()
.url("http://localhost:8080")
.description("Development server"));
}
}Application Properties
# application.yml
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html
operationsSorter: method # Sắp xếp theo HTTP method
tagsSorter: alpha # Sắp xếp tags theo alphabet
displayRequestDuration: true
filter: true # Bật search filter
tryItOutEnabled: true # Bật "Try it out" mặc định
show-actuator: false # Ẩn actuator endpoints
packages-to-scan: com.example.controller
paths-to-match: /api/** # Chỉ document /api/* paths3. Document Controllers
Annotations cơ bản
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/api/v1/products")
@Tag(name = "Products", description = "Các thao tác quản lý sản phẩm")
public class ProductController {
@GetMapping
@Operation(
summary = "Liệt kê tất cả sản phẩm",
description = "Trả về danh sách sản phẩm phân trang với bộ lọc tùy chọn"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Lấy sản phẩm thành công"),
@ApiResponse(responseCode = "400", description = "Tham số request không hợp lệ")
})
public ResponseEntity<Page<ProductResponse>> getProducts(
@Parameter(description = "Lọc theo danh mục")
@RequestParam(required = false) String category,
@Parameter(description = "Số trang (bắt đầu từ 0)", example = "0")
@RequestParam(defaultValue = "0") int page,
@Parameter(description = "Kích thước trang (tối đa 100)", example = "20")
@RequestParam(defaultValue = "20") int size) {
return ResponseEntity.ok(productService.findAll(category, page, size));
}
@GetMapping("/{id}")
@Operation(summary = "Lấy sản phẩm theo ID")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Tìm thấy sản phẩm"),
@ApiResponse(responseCode = "404", description = "Không tìm thấy sản phẩm")
})
public ResponseEntity<ProductResponse> getProduct(
@Parameter(description = "ID sản phẩm", required = true, example = "123")
@PathVariable Long id) {
return ResponseEntity.ok(productService.findById(id));
}
@PostMapping
@Operation(summary = "Tạo sản phẩm mới")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "Tạo sản phẩm thành công"),
@ApiResponse(responseCode = "400", description = "Dữ liệu đầu vào không hợp lệ"),
@ApiResponse(responseCode = "409", description = "Sản phẩm đã tồn tại")
})
public ResponseEntity<ProductResponse> createProduct(
@RequestBody @Valid ProductRequest request) {
ProductResponse product = productService.create(request);
return ResponseEntity
.status(HttpStatus.CREATED)
.header("Location", "/api/v1/products/" + product.id())
.body(product);
}
}Nhóm với Tags
// Tổ chức endpoints thành các nhóm logic
@Tag(name = "Products", description = "Các thao tác CRUD sản phẩm")
@Tag(name = "Inventory", description = "Quản lý kho hàng")
public class ProductController {
@GetMapping
@Operation(tags = {"Products"})
public ResponseEntity<Page<Product>> getProducts() { /* ... */ }
@GetMapping("/{id}/stock")
@Operation(tags = {"Inventory"})
public ResponseEntity<StockInfo> getStock(@PathVariable Long id) { /* ... */ }
}Ẩn Endpoints
// Ẩn endpoints cụ thể khỏi tài liệu
@Operation(hidden = true)
@GetMapping("/internal/health")
public ResponseEntity<String> internalHealth() {
return ResponseEntity.ok("OK");
}
// Hoặc loại trừ toàn bộ controllers
@Hidden
@RestController
@RequestMapping("/internal")
public class InternalController {
// Tất cả endpoints đều bị ẩn
}4. Document Request/Response Models
Schema Annotations
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "Request tạo/cập nhật sản phẩm")
public record ProductRequest(
@Schema(
description = "Tên sản phẩm",
example = "MacBook Pro 16\"",
minLength = 3,
maxLength = 100,
requiredMode = Schema.RequiredMode.REQUIRED
)
@NotBlank
@Size(min = 3, max = 100)
String name,
@Schema(
description = "Giá sản phẩm (USD)",
example = "2499.99",
minimum = "0.01",
maximum = "999999.99"
)
@NotNull
@DecimalMin("0.01")
BigDecimal price,
@Schema(
description = "Danh mục sản phẩm",
example = "electronics",
allowableValues = {"electronics", "clothing", "books", "home", "sports"}
)
@NotBlank
String category,
@Schema(
description = "Mô tả sản phẩm",
example = "MacBook Pro mạnh mẽ nhất từ trước đến nay",
maxLength = 1000,
nullable = true
)
@Size(max = 1000)
String description
) {}Response Model với Examples
@Schema(description = "Object response sản phẩm")
public record ProductResponse(
@Schema(description = "ID sản phẩm duy nhất", example = "12345")
Long id,
@Schema(description = "Tên sản phẩm", example = "MacBook Pro 16\"")
String name,
@Schema(description = "Giá (USD)", example = "2499.99")
BigDecimal price,
@Schema(description = "Danh mục sản phẩm", example = "electronics")
String category,
@Schema(description = "Tình trạng còn hàng")
Boolean inStock,
@Schema(description = "Thời gian tạo", example = "2026-02-07T10:30:00Z")
LocalDateTime createdAt
) {}5. Thêm Request/Response Examples
Inline Examples
@PostMapping
@Operation(summary = "Tạo đơn hàng")
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Request tạo đơn hàng",
required = true,
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = OrderRequest.class),
examples = {
@ExampleObject(
name = "Đơn hàng đơn giản",
summary = "Đơn hàng một sản phẩm",
value = """
{
"customerId": 123,
"items": [
{"productId": 456, "quantity": 1}
],
"shippingAddress": {
"street": "123 Lý Thường Kiệt",
"city": "Hồ Chí Minh",
"zipCode": "70000"
}
}
"""
),
@ExampleObject(
name = "Nhiều sản phẩm",
summary = "Đơn hàng nhiều sản phẩm",
value = """
{
"customerId": 123,
"items": [
{"productId": 456, "quantity": 2},
{"productId": 789, "quantity": 1}
],
"shippingAddress": {
"street": "456 Nguyễn Huệ",
"city": "Hà Nội",
"zipCode": "10000"
},
"couponCode": "GIAM10"
}
"""
)
}
)
)
public ResponseEntity<OrderResponse> createOrder(@RequestBody @Valid OrderRequest request) {
// ...
}6. Document Authentication
JWT Bearer Token
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info().title("Secure API").version("1.0.0"))
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
.components(new Components()
.addSecuritySchemes("bearerAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("Nhập JWT token từ /api/auth/login")));
}
}API Key Authentication
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("apiKey", new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("X-API-Key")
.description("API key để xác thực")));
}Security cấp Endpoint
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
// Endpoint công khai - không cần auth
@GetMapping
@Operation(summary = "Liệt kê sản phẩm", security = {})
public ResponseEntity<List<Product>> getProducts() { /* ... */ }
// Yêu cầu authentication
@PostMapping
@Operation(
summary = "Tạo sản phẩm",
security = @SecurityRequirement(name = "bearerAuth")
)
public ResponseEntity<Product> createProduct(@RequestBody ProductRequest request) { /* ... */ }
}7. Tùy chỉnh Swagger UI
Cấu hình giao diện
# application.yml
springdoc:
swagger-ui:
displayOperationId: false
displayRequestDuration: true
defaultModelsExpandDepth: 1
defaultModelExpandDepth: 3
docExpansion: none # none, list, hoặc full
filter: true
operationsSorter: alpha # alpha hoặc method
tagsSorter: alpha
tryItOutEnabled: true
syntaxHighlight:
activated: true
theme: monokaiNhóm APIs
@Configuration
public class OpenApiConfig {
// Nhóm: Public APIs (v1)
@Bean
public GroupedOpenApi publicApiV1() {
return GroupedOpenApi.builder()
.group("public-v1")
.displayName("Public API v1")
.pathsToMatch("/api/v1/**")
.pathsToExclude("/api/v1/admin/**")
.build();
}
// Nhóm: Admin APIs
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.displayName("Admin API")
.pathsToMatch("/api/v1/admin/**")
.build();
}
}8. Bảo mật Documentation trong Production
Tắt trong Production
# application-prod.yml
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: falseBảo vệ bằng Password
@Configuration
@Profile("!prod")
public class SwaggerSecurityConfig {
@Bean
public SecurityFilterChain swaggerSecurityChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/swagger-ui/**", "/v3/api-docs/**")
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}Hiển thị có điều kiện
@Configuration
public class OpenApiConfig {
@Value("${app.swagger.enabled:true}")
private boolean swaggerEnabled;
@Bean
@ConditionalOnProperty(name = "app.swagger.enabled", havingValue = "true", matchIfMissing = true)
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info().title("My API").version("1.0.0"));
}
}9. Tạo OpenAPI Specification
Export trong Build Time
Maven Plugin:
<plugin>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>generate-openapi</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<apiDocsUrl>http://localhost:8080/v3/api-docs</apiDocsUrl>
<outputFileName>openapi.json</outputFileName>
<outputDir>${project.build.directory}</outputDir>
</configuration>
</plugin>Tạo Client SDK
Sử dụng OpenAPI spec đã export để tạo client SDKs:
# Cài đặt OpenAPI Generator
npm install @openapitools/openapi-generator-cli -g
# Tạo TypeScript client
openapi-generator-cli generate \
-i http://localhost:8080/v3/api-docs \
-g typescript-fetch \
-o ./generated/typescript-client
# Tạo Java client
openapi-generator-cli generate \
-i http://localhost:8080/v3/api-docs \
-g java \
-o ./generated/java-client10. Các lỗi thường gặp và giải pháp
1. Annotations không được phát hiện
Vấn đề: Controller endpoints không hiển thị trong Swagger UI.
Giải pháp: Đảm bảo packages được quét:
springdoc:
packages-to-scan: com.example.controller
paths-to-match: /api/**2. Security không được áp dụng
Vấn đề: Nút "Authorize" bị thiếu hoặc không hoạt động.
Giải pháp: Thêm security globally hoặc per-operation:
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
.components(new Components()
.addSecuritySchemes("bearerAuth", new SecurityScheme()...));
}3. Validation Annotations bị bỏ qua
Vấn đề: @NotNull, @Size không phản ánh trong docs.
Giải pháp: Thêm validation dependency:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>4. Khởi động chậm
Vấn đề: Application khởi động chậm do OpenAPI scanning.
Giải pháp: Giới hạn phạm vi quét:
springdoc:
packages-to-scan: com.example.controller
packages-to-exclude: com.example.internalTóm tắt và điểm chính
✅ SpringDoc OpenAPI là lựa chọn hiện đại cho tài liệu API Spring Boot
✅ Zero configuration để bắt đầu—tự động tạo từ controllers
✅ Rich annotations cho phép document parameters, responses, examples
✅ Authentication docs hỗ trợ JWT, API keys, OAuth2
✅ Tùy chỉnh Swagger UI cho branding và usability
✅ Bảo mật trong production bằng cách tắt hoặc password-protect
✅ Export OpenAPI spec để tạo client SDK
✅ Nhóm APIs để tổ chức tốt hơn
Tiếp theo
Bây giờ API của bạn đã được document tốt, khám phá các chủ đề sau:
Tiếp tục Spring Boot series:
- Docker & Kubernetes Deployment - Deploy API đã document
- Microservices với Spring Cloud - Document hệ thống phân tán
- REST API Advanced Patterns - Exception handling & testing
Bài viết Spring Boot liên quan:
- REST API Best Practices - Tiêu chuẩn thiết kế API
- JWT Authentication - Bảo mật API
- Spring Boot Learning Roadmap - Lộ trình học hoàn chỉnh
Nền tảng REST API:
- REST API là gì? - Hướng dẫn hoàn chỉnh về nguyên tắc REST
- Hướng dẫn HTTP Protocol - HTTP methods, status codes, headers
OpenAPI trong các Framework khác:
- Hiểu OpenAPI với FastAPI - Tự động tạo docs trong Python
- FastAPI Learning Roadmap - Framework API hiện đại của Python
Một phần của Spring Boot Learning Roadmap series
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.