API-First Development: Mastering OpenAPI and Swagger Documentation in Java

In modern API-driven development, clear, accurate, and interactive documentation is no longer a luxury—it's a necessity. OpenAPI Specification (OAS) has emerged as the standard for describing RESTful APIs, while Swagger provides the tools to implement and visualize these specifications. This article explores how to leverage both in Java applications to create professional, maintainable API documentation.


Understanding the Relationship: OpenAPI vs. Swagger

It's crucial to understand the distinction between these commonly confused terms:

OpenAPI Specification (OAS):

  • A standard (formerly known as Swagger Specification)
  • A language-agnostic description format for REST APIs
  • Defines the structure of your API in a YAML or JSON file
  • The specification that describes what your API does

Swagger:

  • A set of tools that implement the OpenAPI Specification
  • Includes Swagger UI, Swagger Editor, Swagger Codegen, etc.
  • The implementation that helps you work with OpenAPI

Analogy: OpenAPI is like the blueprint (the specification), while Swagger is the set of tools (hammer, saw, measuring tape) that help you build and work with that blueprint.


Why OpenAPI and Swagger?

  1. Single Source of Truth: Your API specification becomes the contract between frontend and backend teams
  2. Interactive Documentation: Consumers can try API calls directly from the documentation
  3. Automated Client Generation: Generate client SDKs in multiple languages
  4. Testing: Automate API testing based on the specification
  5. Design-First Approach: Design your API before writing code

Spring Boot 3 and Springdoc OpenAPI

With the decline of the original Springfox Swagger library, Springdoc OpenAPI has become the de facto standard for Spring Boot applications. It automatically generates OpenAPI 3 specifications from your Spring Web MVC or Spring WebFlux applications.

Key Dependencies (Maven):

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>

For Spring Boot 3: (requires Java 17+)

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>

Basic Configuration

Minimal Configuration:
Just adding the dependency is enough to get started. By default, you can access:

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

Custom Configuration in application.yml:

springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operations-sorter: method
tags-sorter: alpha
packages-to-scan: com.example.api
default-consumes-media-type: application/json
default-produces-media-type: application/json

Java Configuration:

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 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("Complete E-Commerce Solution API")
.contact(new Contact()
.name("API Support")
.email("[email protected]")
.url("https://example.com"))
.license(new License()
.name("Apache 2.0")
.url("https://springdoc.org"))
.termsOfService("https://example.com/terms"));
}
}

Annotating Your Controllers

Springdoc automatically scans your controllers and generates documentation. However, you can enhance it with OpenAPI annotations.

Basic Controller Example:

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;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "User Management", description = "APIs for managing users")
public class UserController {
@GetMapping
@Operation(
summary = "Get all users",
description = "Retrieves a list of all registered users with pagination"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Successfully retrieved users"),
@ApiResponse(responseCode = "401", description = "Unauthorized access"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
public List<User> getAllUsers(
@Parameter(description = "Page number starting from 0") 
@RequestParam(defaultValue = "0") int page,
@Parameter(description = "Number of items per page") 
@RequestParam(defaultValue = "20") int size) {
// Implementation
return userService.getAllUsers(page, size);
}
@GetMapping("/{id}")
@Operation(summary = "Get user by ID")
public User getUserById(
@Parameter(description = "User ID", required = true, example = "123") 
@PathVariable Long id) {
return userService.getUserById(id);
}
@PostMapping
@Operation(summary = "Create a new user")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "User created successfully"),
@ApiResponse(responseCode = "400", description = "Invalid input data")
})
public User createUser(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "User object to create",
required = true
)
@RequestBody User user) {
return userService.createUser(user);
}
@PutMapping("/{id}")
@Operation(summary = "Update an existing user")
public User updateUser(
@Parameter(description = "User ID") @PathVariable Long id,
@RequestBody User user) {
return userService.updateUser(id, user);
}
@DeleteMapping("/{id}")
@Operation(summary = "Delete a user")
@ApiResponse(responseCode = "204", description = "User deleted successfully")
public void deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
}

Documenting Models/Schemas

Annotating Your DTOs/Entities:

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Schema(description = "User entity representing a system user")
public class User {
@Schema(description = "Unique identifier of the user", example = "123")
private Long id;
@Schema(
description = "User's full name",
requiredMode = Schema.RequiredMode.REQUIRED,
example = "John Doe"
)
@NotBlank
@Size(min = 2, max = 100)
private String name;
@Schema(
description = "User's email address",
requiredMode = Schema.RequiredMode.REQUIRED,
example = "[email protected]"
)
@NotBlank
@Email
private String email;
@Schema(
description = "User's role in the system",
allowableValues = {"ADMIN", "USER", "MODERATOR"},
example = "USER"
)
private String role;
@Schema(description = "User account creation timestamp", example = "2024-01-15T10:30:00Z")
private LocalDateTime createdAt;
// Constructors, getters, and setters
public User() {}
public User(Long id, String name, String email, String role) {
this.id = id;
this.name = name;
this.email = email;
this.role = role;
}
// Standard getters and setters...
}

Advanced Configuration and Security

Adding JWT Authentication:

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiSecurityConfig {
@Bean
public OpenAPI customOpenAPIWithSecurity() {
final String securitySchemeName = "bearerAuth";
return new OpenAPI()
.addSecurityItem(new SecurityRequirement()
.addList(securitySchemeName))
.components(new Components()
.addSecuritySchemes(securitySchemeName,
new SecurityScheme()
.name(securitySchemeName)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}

Controller Method with Security:

@PostMapping("/secure-data")
@Operation(summary = "Access secure data", security = @SecurityRequirement(name = "bearerAuth"))
public ResponseEntity<String> getSecureData() {
return ResponseEntity.ok("This is secure data");
}

Grouping APIs

For large applications, you might want to group APIs:

Configuration Class:

import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiGroupConfig {
@Bean
public GroupedOpenApi userApi() {
return GroupedOpenApi.builder()
.group("users")
.pathsToMatch("/api/v1/users/**")
.build();
}
@Bean
public GroupedOpenApi productApi() {
return GroupedOpenApi.builder()
.group("products")
.pathsToMatch("/api/v1/products/**")
.build();
}
@Bean
public GroupedOpenApi orderApi() {
return GroupedOpenApi.builder()
.group("orders")
.pathsToMatch("/api/v1/orders/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/api/admin/**")
.build();
}
}

Now you can access different groups:

  • http://localhost:8080/swagger-ui.html (default group)
  • http://localhost:8080/swagger-ui.html?groups=users
  • http://localhost:8080/swagger-ui.html?groups=products

Customizing Swagger UI

application.properties:

# Customize Swagger UI
springdoc.swagger-ui.path=/api-docs
springdoc.swagger-ui.operationsSorter=alpha
springdoc.swagger-ui.tagsSorter=alpha
springdoc.swagger-ui.docExpansion=none
springdoc.swagger-ui.filter=true
springdoc.swagger-ui.tryItOutEnabled=true
springdoc.swagger-ui.displayRequestDuration=true
# API Docs endpoint
springdoc.api-docs.path=/api-docs/json
# Packages to scan
springdoc.packages-to-scan=com.example.api
# Cache control
springdoc.cache.disabled=true

Testing Your Documentation

After setting up, test your endpoints:

  1. Start your Spring Boot application
  2. Access Swagger UI: http://localhost:8080/swagger-ui.html
  3. View raw OpenAPI spec: http://localhost:8080/v3/api-docs
  4. View specific group: http://localhost:8080/v3/api-docs/users

Try these features in Swagger UI:

  • Execute API calls directly from the browser
  • View request/response schemas
  • Test authentication
  • Download the OpenAPI specification

Best Practices

  1. Be Descriptive: Use meaningful summaries and descriptions
  2. Document Errors: Always document possible error responses
  3. Use Examples: Provide examples for request bodies and parameters
  4. Keep Updated: Ensure documentation matches your actual API behavior
  5. Versioning: Include API version in your documentation
  6. Security: Document authentication requirements clearly
  7. Pagination: Document pagination parameters consistently
// Good example with comprehensive documentation
@Operation(
summary = "Search products",
description = "Search products with filtering, sorting, and pagination. " +
"Supports partial text matching on name and description."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Products retrieved successfully"),
@ApiResponse(responseCode = "400", description = "Invalid search parameters"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})

Production Considerations

Disable in Production:

# application-prod.yml
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: false

Or Conditionally Enable:

@Configuration
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true")
public class OpenApiConfig {
// Configuration only loaded when enabled
}

Conclusion

OpenAPI and Swagger provide a powerful combination for API documentation in Java:

  • Springdoc OpenAPI seamlessly integrates with Spring Boot
  • Automatic Generation reduces maintenance overhead
  • Interactive Documentation improves developer experience
  • Standardized Specifications enable API-first development

By implementing comprehensive OpenAPI documentation, you create:

  • Better developer experience for API consumers
  • Clear contracts between frontend and backend teams
  • Automated testing and client generation capabilities
  • Professional, maintainable API documentation

The investment in proper API documentation pays dividends throughout the development lifecycle, making your APIs more discoverable, testable, and consumable.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper