API-first with the Open API Generator
When server-client applications were written in the past, there usually had to be some work done around the API contracts. The Backend and the Frontend agreed (or should have agreed) upon a contract, then both sides implemented their respective models, services, controllers etc. In the good-case scenario, only a few problems would be later found during integration, like differently named fields. In the bad-case scenario, entire sections of the code would be essentially incompatible, requiring a lot of tinkering. And the issues would get worse if changes were later needed. Such problems used to be very common, and although the API-first approach, together with API generators, does not remove them entirely, it does reduce them significantly, brining structure and order to the communication-related code.
The API-first approach
In this development approach, we prioritize the APIs. We recognise that they are essential for the project and that they constitute a product on their own. We need to carefully plan them, taking into account every part of the system – they aren’t tactical by-products by the backend, but rather a contract and facade for the entire application’s design. With the API-first approach, we can use an API description language, like YAML with the OpenAPI Specification, to create common specifications, that then can be used by both backend, frontend and mobile, ensuring the use of the same contracts.
Advantages of the API-first approach with a common OpenAPI Specification:
- Ensures the use of the same contracts by all the parts of the system.
- Facilitates cooperation between the backend and the frontend, facilitating parallel development.
- Automates generation of repeatable elements like models, services and controllers, reducing the amount of boilerplate code, organising the structure and saving developers’ time.
- Automatically generates and enables API documentation.
The Open API YAML specification
This is an example of one of the most common API description languages, open-api 3.0 with YAML:
openapi: "3.0.0"
info:
version: 2.0.0
title: Example Task API
description: |
This specification contains example Task endpoints
servers:
- url: http://localhost:8080
paths:
/task:
description: |
Create new task
post:
tags:
- "Task"
operationId: createTask
requestBody:
description: Create a new task
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateTaskRequest'
example:
{
"name": "Example Task",
"description": "Example Task Description",
"priority": 1
}
responses:
"200":
description: Ok. The successful response contains ID of the newly created task
content:
application/json:
schema:
$ref: "#/components/schemas/CreateTaskResponse"
example:
{
"id": "bd468b42-f06a-4b22-aa8a-2f0c16fe60b4"
}
get:
tags:
- "Task"
operationId: getTasks
parameters:
- name: name
in: query
description: task name filter
allowEmptyValue: true
schema:
type: string
example: Task
- name: priority
in: query
description: task priority filter
allowEmptyValue: true
schema:
type: integer
example: 1
responses:
"200":
description: Ok. The successful response contains the list of 'FileGroupDTO's
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Task"
example:
[
{
"id": "bd468b42-f06a-4b22-aa8a-2f0c16fe60b4",
"name": "Task 1",
"priority": 1,
"description": "Task 1 description"
},
{
"id": "cd468b42-f06a-4b22-aa8a-2f0c16fe60b4",
"name": "Task 2",
"priority": 2,
"description": "Task 2 description"
}
]
components:
schemas:
CreateTaskRequest:
type: object
properties:
name:
type: string
minLength: 2
maxLength: 64
priority:
type: integer
description:
type: string
required:
- name
- priority
CreateTaskResponse:
type: object
properties:
id:
type: string
required:
- id
Task:
$ref: "./components/Task.yml"
We can notice three main parts of the file: the metadata part, the paths and the components section. I will focus on these three sections, although there are many additional OpenApi Objects supported by the specification. A full list can be found here.
Meta data
Here we have information about: – the used specification’s version (openapi: “3.0.0”). – this specific API’s version, name and description (info). – the server’s addresses (servers). Some client generation tools will set these addresses as default ones. Java generators usually ignore this part.
Paths
This is the most important part, where we define our endpoints. For each path we can define multiple methods. The definition of the method can consist of: – tags: A logical grouping of operations qualifier. Many generation tools will use this field to name services and assign methods to them. – operationId: A unique string used to identify the operation. Many generation tools will use this to name the actual service’s method. – optional parameters: A list of parameters (query, path, headers) applicable for the operation. – an optional request body: The request body applicable for this operation. – responses: A list of responses, identified by the http response status (there may be different definitions for different codes). For a specific status code we can add a description and content definition (like a response body), together with an example in json.
Components
This is a little “library” for our API definition. W can define reusable objects here, like models, which we can reference later. Without explicit references, this part has no effect on the API.
Linking objects from other files
For organisational and refactoring purposes we can define OpenAPI objects in other, external files and link them to the main file like this:
$ref: "./components/Task.yml"
This is especially useful when our API specification is becoming large. An external file could have a following content:
type: object
properties:
id:
type: string
name:
type: string
priority:
type: integer
description:
type: string
required:
- id
- name
- priority
Using the Open API Generator in Spring
In order to add the OpenAPI Generator to our Spring project, one of the best OpenAPI generators, we only need to add one plugin to our maven build configuration:
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>6.6.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>
${project.basedir}/src/main/resources/spec/task-api.yml
</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>com.jblog.openapiexample.api</apiPackage>
<modelPackage>com.jblog.openapiexample.model</modelPackage>
<supportingFilesToGenerate>
ApiUtil.java
</supportingFilesToGenerate>
<configOptions>
<useSpringBoot3>true</useSpringBoot3>
<delegatePattern>true</delegatePattern>
<openApiNullable>false</openApiNullable>
<interfaceOnly>false</interfaceOnly>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
By default, the tool generates models, api interfaces and controllers for us. The generated code will already have validation and swagger annotations.
Configurations
In the plugin’s configuration section, we need to specify our specification’s location. This will usually be a resource file. We can then set the generated api’s and model’s packages and optionally provide additional config options. The tool is very versatile and there is a lot of customization available. The full list can be found here. Worth further mention:
- interfaceOnly: If we set this to true, only models and api interfaces will be generated, leaving us to declare our own custom controllers.
- delegatePattern: if we set it to true, the generator will set up a delegate interface for us, which will be injected into the controller and called by it.
- useSpringBoot3: Specifies that code compatible with Spring Boot 3 should be generated, so jakarta instead of javax, etc.
- openApiNullable: Specifies whether we want to use the open api nullable functionality. If we don’t have the dependency for it, we need to set it to false.
The generated Code
After running the mvn clean install
command, we can see that our code was generated correctly:
We can now declare a service implementing the generated delegate, so that our endpoints return something else than the NOT_IMPLEMENTED status.
@Slf4j
@Service
public class TaskHandler implements TaskApiDelegate {
@Override
public ResponseEntity<CreateTaskResponse> createTask(CreateTaskRequest createTaskRequest) {
log.info("Handling create task request");
return ResponseEntity.ok(new CreateTaskResponse(UUID.randomUUID().toString()));
}
@Override
public ResponseEntity<List<Task>> getTasks(String name, Integer priority) {
log.info("Handling get tasks request");
return ResponseEntity.ok(List.of(new Task(UUID.randomUUID().toString(), "Task Name", 1)));
}
}
Testing
Now our application correctly responds to the requests:
Conclusion
Using API-first with the OpenAPI generator in Spring not only lets us separate and refine the communication logic with YAML specifications, facilitating team cooperation, but it also greatly reduces the amount of time and energy we have to spend on boilerplate REST code. With simple configuration and versatility, the OpenAPI generator can be a great addition to the technological stack of our application.