Project that we are building
We are going to build a todo list API, where we will have a Project that can have multiple topics. Each of the topics will have individual tasks.
A project could have one or more topics, and a topic will have one or more tasks.
In this post, we will have the project set up and API specification defined. We will then write some basic test, and integrating the generated code with our project. We will do it in the following sequences:
- Defining the API with
OpenAPI3
specification. - Cover a little bit about the common parts of the
OpenAPI3
- Generate API code in spring boot using openapi-generator-maven-plugin (we will stick to maven now)
- Write tests to make sure our application works the way we want it.
If you are in a rush, and you prefer to see the ending instead, have a look at this repo instead. It has the end state of this post.
OpenAPI 3 Specification
Introduction
I summarize the definition from the official website as follows:
OpenAPI Specification is an API description format for REST APIs. An OpenAPI file allows you to describe your entire API, including available endpoint and operations, operation, examples, authentication method and other supporting information such as contact name, or host.
Having a standard for defining API, enables us to do a lot more automation utilizing those templates such as:
- Generating API Documentation
- Stubs/Mocks
- Communication tools for stakeholders
- Controlling changes of API version.
We can write OpenAPI3 specifications in JSON/YAML format. But in this post, we will write the definition in YAML format (I think it's easier for the eye).
To help us writing the specs, we can use swagger editor online or locally using docker image.
Swagger Editor
Swagger Editor helps us write API Specification with some UI to ease our development.
Swagger editor available online in editor.swagger.io or if you want it to be in your local, you can spin up the docker container
docker pull swaggerapi/swagger-editor
docker run -d -p 80:8080 swaggerapi/swagger-editor
We will use swagger-editor throughout the post.
Defining the API
Metadata & Servers
This is useful to give information about the API and possibly the location of the API servers, so the user could have a try out on the API to the endpoints defined in the server section. The specification looks as follows:
# OpenAPI 3 has quite a difference with OpenAPI 2, the way the linter works in the editor will be based on this.
openapi: 3.0.0
# This section will contain metadata information about the API. It will be shown on the editor/UI when we publish this
info:
title: To-Do Board API
version: 1.0.0
description: API specification to support daily to-do list operations
# The list of the servers that we can use to use the API. This will be rendered as a combo box in the editor/UI when we publish it, so the user could pick and use the endpoint they prefer.
servers:
- url: http://dev-api.todoboard.com/v1
description: DEV-ENV
- url: http://api.todoboard.com/v1
description: PROD-ENV
paths: {}
You may put those in swagger editor and you will see a bit of progress there. We move on!
Paths & Operation
In this section, we are putting a little bit more functionality to our API. We will create an endpoint to retrieve a project as well as topics from a project.
Let's replace paths: {}
from the previous section to be a little more meaningful.
paths:
/projects: # the API path in which the operation will be performed. When this endpoint triggered, the operation meant to be executed.
get: # the HTTP method
parameters:
- in: query
description: find project by name
name: name
schema:
type: string
operationId: searchProjects # optional unique string used to identify an operation. When the code generated, this will be the method name.
tags:
- Projects # group operations logically by resources or any other qualifier. In the context of spring code generation, tags is going to be the class name.
description: retrieve all available projects in paginated way
responses:
200:
description: successfully retrieved all projects
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ProjectResponse'
/projects/{projectId}:
get:
operationId: getProjects
tags:
- Projects
description: retrieve a project by id
parameters:
- name: projectId
in: path
schema:
type: integer
format: int64
minimum: 1
required: true
allowEmptyValue: false
example: 1
responses:
200:
description: the specific project successfully retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/ProjectResponse'
There are few things that quite important here:
- the individual path I think this is pretty self-explanatory, this will be the API path in which the operation will be performed. When this endpoint triggered, the operation meant to be executed.
- method of operation This is the HTTP method. OpenAPI 3.0 supports get, post, put, patch, delete, head, options, and trace.
- operationId operationId is an optional unique string used to identify an operation. If provided, these IDs must be unique among all operations described in your API. In the context of spring code generation, operationId will be the method name that we will need to implements.
- Tags used to group operations logically by resources or any other qualifier. In the context of spring code generation, tags is going to be the class name.
- Parameters OpenAPI 3.0 supports operation parameters passed via path, query string, headers, and cookies. We can have some specifications of the parameter, e.g whether one is required, data type, etc.
- Schema we can define an object that we are going to use in our request/response in a schema. It is good if we have repeated objects that we can use in multiple places
Components
If you notice, both of the responses in both paths above referring to a component called Projects
which, the editor might be complaining because we haven't defined it yet.
Let's update our code as follows:
components:
schemas:
ProjectResponse:
type: object
properties:
id:
type: integer
format: int64
description: unique id of the project for the identifier
example: 1
name:
type: string
description: the name of the project
example: To-do Application Development
Now that we have the OpenAPI3
definition ready, we will use that as the basis of our operation. Some parts of our code will be automatically generated based on the specification. We will talk more details in the next section.
Spring Boot Application
Although not all the functionality exists in the API definition (we will save that for another post), we can get started to write the application. Here is what we are gonna do:
- Set up maven project with few dependencies as follows:
- spring-boot-starter-web Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container
- spring-boot-starter-test (we can exclude the vintage engine) Starter for testing Spring Boot applications with libraries including JUnit, Hamcrest, and Mockito
- springfox-swagger2 we will use the Springfox implementation of the Swagger specification.
- Set up maven build with the following plugins
- spring-boot-maven-plugin The Spring Boot Maven Plugin provides Spring Boot support in Maven, allowing you to package executable jar or war archives and run an application “in-place”.
- openapi-generator-maven-plugin A Maven plugin to support the OpenAPI generator project
- Write an integration test to verify the API is working as per expected using WebMvcTest & MockMvc
Project Setup
We can use Spring Initializer to create our project template. We can add Spring Web dependency, so that spring initializer adds half of our needed dependencies.
Add the remaining dependency so the pom.xml
looks like the following snippet (ignoring some parts of the pom to save the space, refer to the repository for the exact code):
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Required for swagger code generation-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--
* This plugin is used to handle the code generation with the configuration defined in openapi-generator-config.json
* the openapi specification will be placed in api.yaml as an imput spec
-->
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>3.3.4</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/docs/api.yaml</inputSpec>
<configurationFile>${project.basedir}/openapi-generator-config.json</configurationFile>
<ignoreFileOverride>${project.basedir}/.openapi-generator-ignore</ignoreFileOverride>
<generatorName>spring</generatorName>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
API Code Generation
As mentioned in the pom, there are few files we need to take care of as follows:
-
api.yml this is where we place all of the defined API that we worked on earlier. In our case, we will put it in
/main/docs/api.yaml
. - openapi-generator-config.json This file contains the configuration for the openapi-codegen to know what and how to generate the code. Refer to the official documentation for available options. In our case, we will put it in the main project directory.
- .openapi-generator-ignore we want to be a little bit more flexible and exclude some of the classes that we want to define ourselves, e.g Controller classes. In our case, we will put it in the main project directory.
openapi-generator-config.json
This file contains the configuration on how the openapi generator will behave. We can put this configuration under <configuration>
in the pom, but I thought its easier to manage it externally.
For other configuration refer to the official repository
{
"java8": "true",
"dateLibrary": "java8",
"useBeanValidation": "true",
"useTags": "true",
"generateApiTests": "false",
"generateApiDocumentation": "false",
"supportingFiles": "false",
"serializableModel": "true",
"basePackage": "com.alfonzjanfrithz.todoboard",
"modelPackage": "com.alfonzjanfrithz.project.todoboard.model",
"apiPackage": "com.alfonzjanfrithz.project.todoboard.api",
"configPackage": "com.alfonzjanfrithz.project.todoboard.config"
}
.openapi-generator-ignore
And because we don't want to use everything from the generated class, we can actually get a few files ignored by mentioning it in the file that we put in <ignoreFileOverride>
.
**/OpenAPI2SpringBoot.java
**/*Controller.java
Implementation
Write an Integration Test
Time to put some implementation in the code, I like to build a habit to write a test first before real implementation. We will put some basic implementation on the controller, write a test, and fix the test along the way.
We can now implement the *Api interfaces generated from swagger, so we don't need to implement things like @RequestMapping
and all the parameters/headers. The plugins generated those boilerplate and hide it in the interfaces it generates. As they have the default implementation in the interface, we can leave the implementation blank and implement it later.
// /main/controller/ProjectController.java
@Controller
public class ProjectController implements ProjectsApi {
}
// /test/controller/ProjectController.java
@WebMvcTest
class ProjectControllerIT {
@Autowired
private MockMvc mockMvc;
@Test
@DisplayName("OK Response when request sent to /projects endpoint")
public void shouldReturnOkOnProjectsEndpoint() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/projects"))
.andExpect(status().isOk());
}
}
We will get fail test now because the method that is not implemented:
java.lang.AssertionError: Status expected:<200> but was:<501>
Expected :200
Actual :501
Controller implementation
@Controller
public class ProjectController implements ProjectsApi {
@Override
public ResponseEntity<List<ProjectResponse>> searchProjects(@Valid String name) {
return ResponseEntity.ok(Collections.emptyList());
}
}
Our test is passed now and we can follow up with more tests and implementations.
If you are interested in further implementation that based on this project, have a look at the following post:
Top comments (2)
Great article! I noticed in some similar work I am working on there is a generated source file ApiCallback.java. This is an interface with methods like onSuccess, onFailure, etc.. Is it possible to write an implementation to override the behavior? What I am trying to get to his the data sent in the header on successful calls. It seems I would do it this way, but I am open to what makes sense. Thanks in advance..