Introduction
In today's software landscape, designing robust and scalable REST APIs is a crucial aspect of application development. With the growing complexity of APIs, ensuring consistency and maintainability across different services becomes challenging. This is where OpenAPI (formerly known as Swagger) comes into play. OpenAPI provides a standard way to define your API specifications, making it easier to generate client SDKs, server stubs, and documentation automatically.
In this post, we’ll explore how to design REST APIs using OpenAPI and generate a RestController class from an OpenAPI YAML file. We’ll leverage Spring Boot 3.3.3 and Java 21 to build a modern, efficient, and maintainable API.
Setting Up Your Spring Boot Project
For this example, we’ll use Spring Boot 3.3.3 and Java 21.
Project Structure
Once your project is set up, the basic structure will look like this:
demo-open-api-swager
|-- src
| |-- main
| |-- java
| |-- com
| |-- henry.openapi
| |-- service
|-- src
| |-- test
| |-- java
| |-- com
| |-- henry
| |-- ...
|-- pom.xml
Defining the OpenAPI Specification
Now, let’s define our API using an OpenAPI YAML file. This file will describe the API’s endpoints, request and response models, and other relevant details.
1. Create the YAML File
In the src/main/resources directory, create a file named api.yaml and define your API specification. Here’s a simple example:
openapi: 3.0.0
info:
title: Sample API
version: 1.0.0
paths:
/items:
get:
summary: Get all items
operationId: getAllItems
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Item'
post:
summary: Create a new item
operationId: createItem
requestBody:
description: Item to create
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Item'
responses:
'201':
description: Item created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Item'
/items/{id}:
get:
summary: Get an item by ID
operationId: getItemById
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: int64
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/Item'
'404':
description: Item not found
put:
summary: Update an item by ID
operationId: updateItemById
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: int64
requestBody:
description: Item to update
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Item'
responses:
'200':
description: Item updated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Item'
'404':
description: Item not found
delete:
summary: Delete an item by ID
operationId: deleteItemById
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: int64
responses:
'204':
description: Item deleted successfully
'404':
description: Item not found
components:
schemas:
Item:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
required:
- name
2. YAML Content Explanation
info: Provides metadata about the API, such as title and version.
paths: Defines the available API endpoints. In this case, we have a /items endpoint with a CRUD operation.
components: Specifies reusable components like schemas. Here, Item is defined with id and name properties.
Configuring Swagger Codegen Plugin
To generate Java classes from the YAML file, we’ll configure the Swagger Codegen Maven plugin.
1. Maven Plugin Configuration
Add the following configuration to your pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.henry</groupId>
<artifactId>demo-open-api-swager</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo-open-api-swager</name>
<description>Designing Robust REST APIs with OpenAPI in Spring Boot</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- Spring Boot Starter for Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot DevTools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- Lombok for reducing boilerplate -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Swagger Models and Annotations -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-models</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
<version>2.2.22</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.16</version>
</dependency>
<!-- Nullable support for Jackson -->
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.6</version>
</dependency>
<!-- Springdoc OpenAPI for Spring Boot 3 instead of springfox dependencies -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>
<!-- Jakarta Validation API -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<!-- Jakarta Annotations API -->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<!-- Spring Boot Starter for Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot Maven Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- OpenAPI Generator Plugin -->
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<!-- Till 6.4.0 we can use jakarta instead of javax with <useJakartaEe>true</useJakartaEe>-->
<version>7.8.0</version>
<executions>
<execution>
<id>generate-sources</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api.yaml</inputSpec>
<output>${project.basedir}/src/main/generated-sources</output>
<generatorName>spring</generatorName>
<apiPackage>com.henry.openapi.controller</apiPackage>
<modelPackage>com.henry.openapi.model</modelPackage>
<invokerPackage>com.henry.openapi</invokerPackage>
<generateSupportingFiles>false</generateSupportingFiles>
<skipOperationExample>true</skipOperationExample>
<generateApis>true</generateApis>
<generateModelTests>false</generateModelTests>
<generateModelDocumentation>false</generateModelDocumentation>
<configOptions>
<useJakartaEe>true</useJakartaEe>
<serializableModel>true</serializableModel>
<dateLibrary>legacy</dateLibrary>
<java21>true</java21>
<library>spring-boot</library>
<delegatePattern>true</delegatePattern>
<useBeanValidation>true</useBeanValidation>
<useOptional>false</useOptional>
<hideGenerationTimestamp>true</hideGenerationTimestamp>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
<!-- Maven Compiler Plugin for Java 21 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Generating REST Controllers
With the YAML file and Maven plugin configured, we can now generate the REST controllers.
1. Run the Maven Command
Execute the following command in your project directory:
mvn clean install
2. Review the Generated Code
package com.henry.openapi.controller;
import com.henry.openapi.model.Item;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.constraints.*;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import jakarta.annotation.Generated;
/**
* A delegate to be called by the {@link ItemsApiController}}.
* Implement this interface with a {@link org.springframework.stereotype.Service} annotated class.
*/
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.8.0")
public interface ItemsApiDelegate {
default Optional<NativeWebRequest> getRequest() {
return Optional.empty();
}
/**
* POST /items : Create a new item
*
* @param item Item to create (required)
* @return Item created successfully (status code 201)
* @see ItemsApi#createItem
*/
default ResponseEntity<Item> createItem(Item item) {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
/**
* DELETE /items/{id} : Delete an item by ID
*
* @param id (required)
* @return Item deleted successfully (status code 204)
* or Item not found (status code 404)
* @see ItemsApi#deleteItemById
*/
default ResponseEntity<Void> deleteItemById(Long id) {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
/**
* GET /items : Get all items
*
* @return Successful response (status code 200)
* @see ItemsApi#getAllItems
*/
default ResponseEntity<List<Item>> getAllItems() {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
/**
* GET /items/{id} : Get an item by ID
*
* @param id (required)
* @return Successful response (status code 200)
* or Item not found (status code 404)
* @see ItemsApi#getItemById
*/
default ResponseEntity<Item> getItemById(Long id) {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
/**
* PUT /items/{id} : Update an item by ID
*
* @param id (required)
* @param item Item to update (required)
* @return Item updated successfully (status code 200)
* or Item not found (status code 404)
* @see ItemsApi#updateItemById
*/
default ResponseEntity<Item> updateItemById(Long id,
Item item) {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}
Customizing the Generated Code
Although the generated code provides a good starting point, you may want to customize it to suit your application's specific needs.
1. Implementing the Interface
You can create a class that implements the ItemsApiDelegate interface and adds custom logic:
package com.henry.openapi.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import com.henry.openapi.controller.ItemsApiDelegate;
import com.henry.openapi.model.Item;
@Service
public class ItemsServiceImpl implements ItemsApiDelegate{
// Mock in-memory database
private final List<Item> items = new ArrayList<>();
private Long currentId = 1L;
public ItemsServiceImpl() {
// Initialize with some mock data
var it1 = new Item();
it1.setId(1L);
it1.setName("Item 1");
items.add(it1);
var it2 = new Item();
it2.setId(2L);
it2.setName("Item 2");
items.add(it2);
}
@Override
public ResponseEntity<List<Item>> getAllItems() {
return new ResponseEntity<>(items, HttpStatus.OK);
}
@Override
public ResponseEntity<Item> getItemById(Long id) {
Optional<Item> item = items.stream()
.filter(i -> i.getId().equals(id))
.findFirst();
return item.map(i -> new ResponseEntity<>(i, HttpStatus.OK))
.orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
@Override
public ResponseEntity<Item> createItem(Item item) {
item.setId(currentId++); // Simulate auto-increment ID
items.add(item);
return new ResponseEntity<>(item, HttpStatus.CREATED);
}
@Override
public ResponseEntity<Item> updateItemById(Long id, Item item) {
for (int i = 0; i < items.size(); i++) {
if (items.get(i).getId().equals(id)) {
item.setId(id); // Keep the same ID
items.set(i, item);
return new ResponseEntity<>(item, HttpStatus.OK);
}
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
@Override
public ResponseEntity<Void> deleteItemById(Long id) {
boolean removed = items.removeIf(i -> i.getId().equals(id));
if (removed) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
}
Testing the Generated API
Now that you have your controllers in place, it’s time to test the API.
1. Run the Application
Start your Spring Boot application using your preferred method:
mvn spring-boot:run
2. Access Swagger UI
Navigate to http://localhost:8080/swagger-ui.html in your browser. Swagger UI will display the endpoints defined in your OpenAPI specification, allowing you to interact with them directly.
3. curl requests for each method in the ItemsServiceImpl class
1. GET /items (Retrieve all items)
curl -X GET "http://localhost:8080/items" -H "accept: application/json"
2. GET /items/{id} (Retrieve an item by ID)
curl -X GET "http://localhost:8080/items/1" -H "accept: application/json"
3. POST /items (Create a new item)
curl -X POST "http://localhost:8080/items" \
-H "Content-Type: application/json" \
-H "accept: application/json" \
-d '{
"name": "New Item"
}'
4. PUT /items/{id} (Update an item by ID)
curl -X PUT "http://localhost:8080/items/1" \
-H "Content-Type: application/json" \
-H "accept: application/json" \
-d '{
"name": "Updated Item"
}'
5. DELETE /items/{id} (Delete an item by ID)
curl -X DELETE "http://localhost:8080/items/1" -H "accept: application/json"
Conclusion
Additional Configuration
IntelliJ IDEA 2024.2.1 Community
Click On -> Apply Button
Eclipse IDE for Enterprise Java and Web Developers 2024-06
2. Check checkbox for "Download external resources like referenced DTD,XSD
References
https://swagger.io/specification/
https://springdoc.org/
https://github.com/swagger-api/swagger-codegen
https://stackoverflow.com/questions/70692260/cvc-elt-1-a-cannot-find-the-declaration-of-element-project
https://stackoverflow.com/questions/70291226/how-to-remove-apiutil-java-from-openapi-geneate-task-with-openapi-generator-grad
https://github.com/springdoc/springdoc-openapi/issues/1977
https://stackoverflow.com/questions/74593513/is-there-a-way-to-configure-openapi-generator-to-use-jakarta-package-during-gene