In this article, we will explore how to implement OpenAPI in a Java Spring Boot project to create documented and interactive REST APIs. OpenAPI (formerly Swagger) has become the de facto standard for REST API documentation.
1. What is OpenAPI?
OpenAPI is a standardized specification that allows you to describe REST APIs in a structured way. It enables you to:
- Automatically document your API
- Generate client and server code
- Validate API contracts
- Create an interactive interface to test the API
2. API-First Approach
The API-First approach consists of defining the OpenAPI specification first before implementing the code. This method offers several advantages:
- Up-to-date documentation: Documentation is always synchronized with implementation
- Code generation: Java code is automatically generated from the specification
- Contract validation: Changes are automatically validated
- Collaboration: Frontend and backend teams can work in parallel
3. Project Configuration
3.1 Gradle Dependencies
Add the following dependencies to your build.gradle
file:
plugins {
id "org.openapi.generator" version "7.0.1"
}
dependencies {
implementation "io.swagger.core.v3:swagger-annotations:2.2.8"
implementation "org.webjars:swagger-ui:4.15.5"
implementation "org.webjars:webjars-locator-lite:0.1.0"
implementation "org.openapitools:jackson-databind-nullable:0.2.6"
}
3.2 OpenAPI Generator Configuration
Create a gradle/openapi.gradle
file with the following configuration:
sourceSets {
main {
java {
srcDir file("${buildDir}/generated-api/src/main/java")
}
}
}
openApiValidate {
inputSpec = "$rootDir/src/main/resources/openapi/api-v1.0.yaml"
}
openApiGenerate {
generatorName = "spring"
library = "spring-boot"
validateSpec = true
inputSpec = "$rootDir/src/main/resources/openapi/api-v1.0.yaml"
outputDir = "$buildDir/generated-api"
invokerPackage = "com.example.api.v1.config"
apiPackage = "com.example.api.v1.web"
modelPackage = "com.example.api.v1.web.model"
modelNameSuffix = "DTO"
configOptions = [
useSpringBoot3: "true",
dateLibrary: "java8",
useOptional: "false",
hideGenerationTimestamp: "true",
interfaceOnly: "true",
useTags: "true",
swaggerDocketConfig: "false",
swaggerAnnotations: "false",
apiFirst: "false"
]
typeMappings = [
"Date": "LocalDate",
"DateTime": "LocalDateTime"
]
importMappings = [
"LocalDate": "java.time.LocalDate",
"LocalDateTime": "java.time.LocalDateTime"
]
}
tasks.openApiGenerate.dependsOn tasks.openApiValidate
compileJava.dependsOn tasks.openApiGenerate
4. OpenAPI File Structure
Organize your OpenAPI files in a modular way:
src/main/resources/openapi/
├── api-v1.0.yaml # Main specification
├── paths/ # Endpoint definitions
│ ├── users.yaml
│ ├── products.yaml
│ └── orders.yaml
├── schemas/ # Data models
│ ├── User.yaml
│ ├── Product.yaml
│ └── Order.yaml
├── responses/ # Standardized responses
│ ├── BadRequest.yaml
│ ├── NotFound.yaml
│ └── InternalServerError.yaml
└── swagger-ui/ # Swagger UI configuration
└── swagger-ui-config.js
5. OpenAPI Specification Definition
5.1 Main File (api-v1.0.yaml)
openapi: "3.0.0"
info:
version: "1.0"
title: "My REST API"
description: "REST API for user and product management"
contact:
name: "Development Team"
email: "dev@example.com"
servers:
- url: "https://api.example.com/v1"
description: "Production server"
- url: "https://localhost:8080/v1"
description: "Development server"
tags:
- name: "Users"
description: "User management"
- name: "Products"
description: "Product management"
paths:
/users:
summary: "List users"
description: "Retrieves the list of all users"
$ref: "./paths/users.yaml"
/products:
summary: "List products"
description: "Retrieves the list of all products"
$ref: "./paths/products.yaml"
components:
schemas:
User:
$ref: "./schemas/User.yaml"
Product:
$ref: "./schemas/Product.yaml"
responses:
BadRequest:
$ref: "./responses/BadRequest.yaml"
NotFound:
$ref: "./responses/NotFound.yaml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
5.2 Endpoint Definition (paths/users.yaml)
get:
tags:
- "Users"
summary: "List users"
description: "Retrieves the list of all users with pagination"
operationId: "getUsers"
parameters:
- name: "page"
in: "query"
description: "Page number"
required: false
schema:
type: "integer"
default: 0
- name: "size"
in: "query"
description: "Page size"
required: false
schema:
type: "integer"
default: 20
responses:
'200':
description: "Users list retrieved successfully"
content:
application/json:
schema:
type: "object"
properties:
content:
type: "array"
items:
$ref: "../schemas/User.yaml"
totalElements:
type: "integer"
totalPages:
type: "integer"
'401':
$ref: '../responses/Unauthorized.yaml'
'500':
$ref: '../responses/InternalServerError.yaml'
post:
tags:
- "Users"
summary: "Create user"
description: "Creates a new user"
operationId: "createUser"
requestBody:
required: true
content:
application/json:
schema:
$ref: "../schemas/User.yaml"
responses:
'201':
description: "User created successfully"
content:
application/json:
schema:
$ref: "../schemas/User.yaml"
'400':
$ref: '../responses/BadRequest.yaml'
'401':
$ref: '../responses/Unauthorized.yaml'
5.3 Model Definition (schemas/User.yaml)
type: "object"
required:
- "id"
- "email"
- "firstName"
- "lastName"
properties:
id:
type: "integer"
format: "int64"
description: "Unique user identifier"
example: 1
email:
type: "string"
format: "email"
description: "User email address"
example: "john.doe@example.com"
firstName:
type: "string"
description: "User first name"
example: "John"
lastName:
type: "string"
description: "User last name"
example: "Doe"
phone:
type: "string"
description: "Phone number"
example: "+1234567890"
createdAt:
type: "string"
format: "date-time"
description: "Creation date"
example: "2023-01-01T10:00:00Z"
updatedAt:
type: "string"
format: "date-time"
description: "Last modification date"
example: "2023-01-01T10:00:00Z"
6. Swagger UI Configuration
6.1 Spring Configuration
Create a configuration class for Swagger UI:
@Configuration
@ConditionalOnProperty(prefix = "swagger-ui", name = "enabled", havingValue = "true")
public class SwaggerUiConfiguration implements WebMvcConfigurer {
@Value("${swagger-ui.configuration-name:default}")
private String configurationName;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler("/openapi/**", "/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/", "classpath:/openapi/")
.resourceChain(true)
.addResolver(new LiteWebJarsResourceResolver())
.addResolver(new PathResourceResolver())
.addTransformer(new SwaggerUiTransformer());
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/swagger-ui/")
.setViewName("forward:/swagger-ui/index.html");
}
}
6.2 JavaScript Configuration (swagger-ui-config.js)
window.onload = function () {
const ui = SwaggerUIBundle({
url: "/openapi/api-v1.0.yaml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
oauth2RedirectUrl: window.location.protocol + "//" +
window.location.hostname + ":" +
window.location.port + "/swagger-ui/oauth2-redirect.html",
withCredentials: true
});
ui.initOAuth({
clientId: "my-client-id",
scopes: "read write",
usePkceWithAuthorizationCodeGrant: true
});
window.ui = ui;
};
7. Controller Implementation
Implement the generated interfaces in your controllers:
@RestController
@RequiredArgsConstructor
@RequestMapping("/v1")
public class UserApiController implements UserApi {
private final UserService userService;
private final UserMapper userMapper;
private final NativeWebRequest nativeWebRequest;
@Override
public Optional<NativeWebRequest> getRequest() {
return Optional.ofNullable(nativeWebRequest);
}
@Override
public ResponseEntity<UsersResponseDTO> getUsers(
@Valid @RequestParam(value = "page", required = false, defaultValue = "0") Integer page,
@Valid @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
Page<User> users = userService.findAll(PageRequest.of(page, size));
UsersResponseDTO response = userMapper.toResponseDTO(users);
return ResponseEntity.ok(response);
}
@Override
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody UserDTO userDTO) {
User user = userMapper.toEntity(userDTO);
User savedUser = userService.save(user);
UserDTO response = userMapper.toDTO(savedUser);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
8. Application Configuration
Add the following configuration to your application.yml
:
swagger-ui:
enabled: true
configuration-name: default
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${OIDC_ISSUER_URI}
9. Benefits of This Approach
- Automatic documentation: Documentation is always up-to-date
- Code generation: Reduces errors and development time
- Contract validation: Changes are automatically validated
- Interactive interface: Swagger UI allows easy API testing
- Collaboration: Teams can work in parallel
- Maintenance: Changes propagate automatically
10. Best Practices
- Modular organization: Separate paths, schemas, and responses
- Use references: Avoid duplication with $ref
- Versioning: Manage your API versions
- Validation: Validate your OpenAPI specification
- Testing: Test your API with generated tools
- Security: Clearly define security schemes
Conclusion
Implementing OpenAPI in a Java Spring Boot project offers many advantages in terms of productivity, quality, and maintenance. The API-First approach allows you to create well-documented and easy-to-use APIs while reducing errors and improving collaboration between teams.
By following the steps described in this article, you will be able to set up a robust and scalable solution for your REST APIs.
0 Comments