loading...

Spring Boot REST with OpenAPI 3

alfonzjanfrithz profile image Alfonz Jan Frithz ・8 min read

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:

Posted on by:

alfonzjanfrithz profile

Alfonz Jan Frithz

@alfonzjanfrithz

DevOps Engineer, code at heart.

Discussion

markdown guide
 

Great! I did not consider that. Until now I always convert the OpenAPI spec to Spring MVC endpoints by hand. I will give it a try..