API Documentation with OpenAPI and Springdoc
Good API documentation is non-negotiable. Springdoc reads your Spring MVC code and auto-generates interactive OpenAPI 3.1 documentation — no separate doc files to maintain.
Setup
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
Start the app and visit:
- Swagger UI:
http://localhost:8080/swagger-ui.html - OpenAPI JSON:
http://localhost:8080/v3/api-docs - OpenAPI YAML:
http://localhost:8080/v3/api-docs.yaml
Springdoc scans your @RestController classes and generates the spec automatically. Zero configuration needed for basic docs.
Configuring the API Info
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("Order Service API")
.description("REST API for managing orders in the DevOpsMonk e-commerce platform")
.version("v1.0.0")
.contact(new Contact()
.name("DevOpsMonk Team")
.email("api@devopsmonk.com")
.url("https://devopsmonk.com"))
.license(new License()
.name("MIT")
.url("https://opensource.org/licenses/MIT")))
.externalDocs(new ExternalDocumentation()
.description("Full documentation")
.url("https://docs.devopsmonk.com"))
.servers(List.of(
new Server().url("https://api.devopsmonk.com").description("Production"),
new Server().url("https://staging-api.devopsmonk.com").description("Staging"),
new Server().url("http://localhost:8080").description("Local")
));
}
}
Documenting Controllers with @Operation
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
@Tag(name = "Orders", description = "Order management operations")
public class OrderController {
private final OrderService orderService;
@GetMapping("/{id}")
@Operation(
summary = "Get order by ID",
description = "Retrieves a single order by its UUID. Returns 404 if not found.",
responses = {
@ApiResponse(responseCode = "200", description = "Order found",
content = @Content(schema = @Schema(implementation = OrderResponse.class))),
@ApiResponse(responseCode = "404", description = "Order not found",
content = @Content(schema = @Schema(implementation = ProblemDetail.class))),
@ApiResponse(responseCode = "401", description = "Unauthorized")
}
)
public ResponseEntity<OrderResponse> getOrder(
@PathVariable @Parameter(description = "Order UUID", example = "550e8400-e29b-41d4-a716-446655440000")
UUID id) {
return orderService.findById(id)
.map(o -> ResponseEntity.ok(OrderResponse.from(o)))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
@Operation(
summary = "Create a new order",
description = "Creates an order. Returns 201 with a Location header pointing to the new order."
)
@ApiResponse(responseCode = "201", description = "Order created successfully")
@ApiResponse(responseCode = "400", description = "Validation failed",
content = @Content(schema = @Schema(implementation = ProblemDetail.class)))
public ResponseEntity<OrderResponse> createOrder(
@RequestBody @Valid
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Order creation request",
required = true,
content = @Content(
schema = @Schema(implementation = CreateOrderRequest.class),
examples = @ExampleObject(
name = "Simple order",
value = """
{
"customerId": "550e8400-e29b-41d4-a716-446655440000",
"items": [
{"productId": "prod-123", "quantity": 2}
],
"shippingAddress": {
"line1": "123 Main St",
"city": "London",
"country": "GB",
"postalCode": "EC1A1BB"
}
}
"""
)
)
)
CreateOrderRequest request) {
Order order = orderService.create(request);
return ResponseEntity
.created(URI.create("/api/orders/" + order.id()))
.body(OrderResponse.from(order));
}
}
Documenting DTO Schemas with @Schema
@Schema(description = "Request body for creating a new order")
public record CreateOrderRequest(
@Schema(description = "UUID of the customer placing the order",
example = "550e8400-e29b-41d4-a716-446655440000",
requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull UUID customerId,
@Schema(description = "List of items to include in the order. Must have at least one item.",
minLength = 1,
requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty List<@Valid OrderItemRequest> items,
@Schema(description = "Promotional code to apply (optional)",
example = "SPRING20",
maxLength = 20)
String promoCode
) {}
@Schema(description = "An item within an order")
public record OrderItemRequest(
@Schema(description = "UUID of the product",
example = "prod-00000001",
requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull UUID productId,
@Schema(description = "Number of units to order",
example = "2",
minimum = "1",
maximum = "999")
@Positive @Max(999) int quantity
) {}
Grouping Endpoints
For larger APIs, split docs into groups:
@Configuration
public class OpenApiGroupConfig {
@Bean
public GroupedOpenApi ordersApi() {
return GroupedOpenApi.builder()
.group("orders")
.displayName("Order Management")
.pathsToMatch("/api/orders/**")
.build();
}
@Bean
public GroupedOpenApi customersApi() {
return GroupedOpenApi.builder()
.group("customers")
.displayName("Customer Management")
.pathsToMatch("/api/customers/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.displayName("Admin Operations")
.pathsToMatch("/api/admin/**")
.addOpenApiCustomizer(openApi ->
openApi.info(new Info().title("Admin API").version("v1")))
.build();
}
}
Navigate groups in Swagger UI via the dropdown in the top right.
Documenting Security
JWT Bearer Authentication
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.addSecurityItem(new SecurityRequirement().addList("Bearer Auth"))
.components(new Components()
.addSecuritySchemes("Bearer Auth",
new SecurityScheme()
.name("Bearer Auth")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("Enter JWT token from /api/auth/login")))
.info(apiInfo());
}
Now Swagger UI shows an “Authorize” button. Click it, paste a JWT, and all subsequent requests include it.
Exclude specific endpoints from auth:
@GetMapping("/health")
@Operation(security = {}) // no security required
public HealthResponse health() { ... }
API Key Authentication
.addSecuritySchemes("API Key",
new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("X-API-Key"))
Documenting Pagination
Springdoc doesn’t auto-document Pageable parameters well. Add explicit parameter docs:
@GetMapping
@Operation(summary = "List orders with pagination")
@Parameters({
@Parameter(name = "page", description = "Page number (0-based)", example = "0",
in = ParameterIn.QUERY, schema = @Schema(type = "integer", defaultValue = "0")),
@Parameter(name = "size", description = "Page size (max 100)", example = "20",
in = ParameterIn.QUERY, schema = @Schema(type = "integer", defaultValue = "20")),
@Parameter(name = "sort", description = "Sort field and direction",
example = "createdAt,desc",
in = ParameterIn.QUERY, schema = @Schema(type = "string"))
})
public PagedResponse<OrderResponse> listOrders(
@Parameter(hidden = true) Pageable pageable) { // hide the Pageable param itself
return PagedResponse.from(orderRepository.findAll(pageable), OrderResponse::from);
}
Configuration Properties
springdoc:
api-docs:
path: /v3/api-docs # OpenAPI JSON endpoint
enabled: true
swagger-ui:
path: /swagger-ui.html # Swagger UI path
enabled: true
operations-sorter: alpha # sort by method name (or 'method' for HTTP method)
tags-sorter: alpha
display-request-duration: true
try-it-out-enabled: true # enable "Try it out" by default
filter: true # show search filter
show-actuator: false # don't show actuator endpoints in docs
packages-to-scan: com.devopsmonk.order # limit scanning
paths-to-exclude: /api/internal/** # hide internal endpoints
Securing the Docs Endpoint
In production, you don’t want Swagger UI publicly accessible. Several options:
Option 1: Disable in production
# application-prod.yml
springdoc:
swagger-ui:
enabled: false
api-docs:
enabled: false
Option 2: Basic auth for docs only
@Configuration
@Profile("!prod")
public class SwaggerSecurityConfig {
@Bean
@Order(1) // higher priority than main security config
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();
}
}
Option 3: IP allowlist via reverse proxy (Nginx)
location /swagger-ui {
allow 10.0.0.0/8; # internal network only
deny all;
proxy_pass http://app:8080;
}
Exporting the Spec for Code Generation
The OpenAPI spec is the source of truth for client SDKs:
# Export as JSON
curl http://localhost:8080/v3/api-docs > openapi.json
# Export as YAML
curl http://localhost:8080/v3/api-docs.yaml > openapi.yaml
Generate a TypeScript client:
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-fetch \
-o ./frontend/src/api-client
Generate a Java client:
openapi-generator generate \
-i openapi.yaml \
-g java \
--library restclient \
-o ./order-service-client
Auto-export on build
<!-- pom.xml — export spec during build -->
<plugin>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<apiDocsUrl>http://localhost:8080/v3/api-docs.yaml</apiDocsUrl>
<outputFileName>openapi.yaml</outputFileName>
<outputDir>${project.build.directory}</outputDir>
</configuration>
</plugin>
What You’ve Learned
- Add
springdoc-openapi-starter-webmvc-ui— Swagger UI appears at/swagger-ui.htmlautomatically - Configure API metadata with a
OpenAPIbean - Document operations with
@Operation,@ApiResponse,@Parameter,@Schema - Group endpoints for large APIs using
GroupedOpenApibeans - Add JWT/API key security schemes and expose the Authorize button in Swagger UI
- Disable docs in production or restrict with auth/IP allowlist
- Export the spec and generate typed clients for frontend/partner teams
This completes Part 2: Building REST APIs. You now have a complete foundation for building production-quality REST APIs. In Part 3, we add persistence with Spring Data JPA.
Next: Article 15 — Introduction to Spring Data JPA — the ORM layer that maps Java objects to database tables.