API-first with the Open API Generator

Filip Raszka

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.

References

Meet the geek-tastic people, and allow us to amaze you with what it's like to work with j‑labs!

Contact us