Contract first with SpringBoot

Clemens Künzel, 20 March 2023

Components of modern software applications use Representational state transfer (REST) Application Programming Interfaces (APIs) for inter-process communication (IPC) in the world of cloud computing. The term REST is described in the dissertation by Roy Fielding as “architectural style for distributed hypermedia systems”.

APIs allow data exchange between distributed software programs. For an error-free data exchange between provider and consumer of an API, it is necessary to make agreements for communication. Contracts as a formal specification represent such an agreement. In principle, they facilitate the development of the consumer side of an API. The contract-first approach also supports the development of the provider side.

The following text first works through the relevant basics. Then it shows an example of how communication between two SpringBoot applications can take place following the contract-first approach. The goal is to use the predefined contract to generate both the SpringBoot server code frame and the client adapter code.

Using a code generator in conjunction with the contract-first approach has the advantage of pushing integration issues into the background. Both the provider and the client of an API can focus on the business logic by just using the generated code.

Basics

The following subsections explain the ideas of IPC with REST over Hypertext Transfer Protocol (HTTP). It describes structuring patterns for data separation between client and server over HTTP. This is followed by a brief classification and introduction to API Contracts. For the definition of the transferred data models a short distinction is made between data models embedded in the specification and externally defined and referenced data models.

HTTP & REST

A REST API uses the HTTP request methods (e.g. PUT, GET, POST and DELETE) to store, read, delete or modify data.

A request to an HTTP server can consist of the following elements:

  • Start-Line: request method, path and HTTP version e.g. GET /geometry HTTP/1.1.
  • URL query parameter: e.g.: color=blue&shape=circle
  • Request header fields: With key-value pairs in ASCII format e.g.: X-Custom-Request-Header: value
  • (Multipart-) Request Body: With further request data.

In contrast, the response of an HTTP server may consist of the following elements:

  • Start-Line: HTTP version and Response code e.g.: HTTP/1.1 200 OK.
  • Response header fields: with key-value pairs in ASCII format e.g.: X-Custom-Response-Header: value
  • (Multipart) Response Body: With additional response data.

API contracts

The list of components of HTTP requests and responses illustrates that HTTP-based data exchange can be realized in various ways. For example, the key-value pair color=blue can be transferred from the client to the server as URL query parameter, in the header or in the request body.

“The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to HTTP APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined, a consumer can understand and interact with the remote service with a minimal amount of implementation logic.”

Source: OAS Version 3.1.0

The specification facilitates the integration of HTTP clients and servers. Manually created API documentation of the API providers, with instructions for request formulation and response structure, becomes obsolete.

History

The history of API contracts is the history of Swagger and OpenAPI. The following table is an attempt to summarize the history of contracts:

Year Month Event Hint
2010 - Tony Tam had the idea to create “SWAGR” at Wordnik
2011 January Tony Tam moved to Reverb Technologies
2011 August Swagger Version 1.0 released
2012 August Swagger Version 1.1 released
2014 March Swagger Version 1.2 released first formal specification
2014 September Swagger Version 2.0 released
2015 March Swagger IP was acquired from Reverb by SmartBear
2015 September Tony Tam moved to SmartBear
2015 December SmartBear launched OpenAPI Initiative (OAI) Swagger Version 2.0 is now OAS Version 2.0
2017 July OAS 3.0.0 released
2017 December OAS Version 3.0.1released
2018 October OAS Version 3.0.2 released
2020 February OAS Version 3.0.3 released
2021 February OAS Version 3.1.0 released

Sources: [1], [2], [3], [4]

Example: Contract

The following minimal example describes an OAS in version 3.1.0 The specification describes a path /endpoint which provides its functionality with the HTTP method GET. If the response is positive (status code 200), data with the media type format application/json is transferred in the response body:

openapi: 3.1.0
info:
  title: Example 1 - OAS
  version: 1.0.0
paths:
  /endpoint:
    get:
      responses:
        200:
          content:
            application/json: {}

A more detailed example can be found in the Code Repository of the OpenAPI Initiative (OAI). The repository contains examples that map a variety of HTTP methods and clearly describe the use of header, query, path, and body parameters.

Code-First vs. Contract-First

With the availability of machine-readable OAS, two distinct streams emerged:

Code First vs. Contract First

Code-First: The OAS is derived from the server implementation. This is usually done by adding annotations to the server code. Through build plugins, when the code is compiled, the annotations are evaluated and converted into an OAS. This contract derived from the code can be shared with the consumer of the API and support or automate client development.

Contract-First: Both client and server implementations are derived from the OAS. This requires the definition of the OAS upfront. For the definition, a browser-based tool exists with the Swagger Editor to describe the communication patterns between client and server. Once the specification is created, the development of client and server implementation can be decoupled.

Model definition

As described above, the request-response behavior of an API can be described with an OAS. It is possible to transfer user data via the body of the request and response. Often, the sent or received payloads of REST APIs are defined as structured models in JavaScript Object Notation (JSON) format with the media type application/json.

It is possible to define payloads in other media type formats in the OAS, for example in the media type application/xml. The OAS provides native integration of model definitions in JSON format. For this reason, the focus of the following text is limited to the JSON format and the associated JSON schema definition.

Basically, model can be referenced in the OAS with the $ref attribute. The following examples are taken from the Swager documentation of the attribute:

  • Embedded model: $ref: '#/components/schemas/myElement'
  • External model (same folder): $ref: 'schemas.json#/myElement'
  • External model (parent folder): $ref: '../schemas.json#/myElement'
  • External model (another folder): $ref: '../another-folder/schemas.json#/myElement'
  • External model (different server): $ref: 'http://path/to/your/schemas.json#/myElement'

This listing shows that models can be deployed embedded as well as locally or remotely.

Embedded vs. External Models

Example: Embedded models

When using embedded models, Components are defined in the OAS. The components defined in the specification can be assigned to the request or response operations via internal references.

The following specification describes a path /endpoint that provides functionality via the HTTP method POST. The specification defines that JSON is used in the request body of the POST operation. The JSON schema of the request is defined in the components section as EmbeddedRequestModel and is mapped to the request body of the operation via the internal reference $ref: '#/components/schemas/EmbeddedRequestModel'.

Furthermore, the specification defines that JSON is returned in the response body of the POST operation if the response is positive (status code 200). The JSON schema of the response is defined in the components section as EmbeddedResponseModel and is mapped to the response body of the operation in case of a positive response via the internal reference $ref: '#/components/schemas/EmbeddedResponseModel':

openapi: 3.1.0
info:
  title: Example 2 - OAS
  version: 1.0.0
paths:
  /endpoint:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmbeddedRequestModel'
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmbeddedResponseModel'
components:
  schemas:
    EmbeddedRequestModel:
      properties:
        description:
          type: string
        name:
          type: string

    EmbeddedResponseModel:
      properties:
        status:
          type: string

Example: External models

When using external models, the JSON models used are defined as external files. External references can be used to assign the external models to the request or response operations. This pattern has the advantage that the same model definitions can be reused in different OAS.

This example assumes the following folder structure:

root/
├── openapi.yml
└── models/
    ├── ExternalRequestModel.json
    └── ExternalResponseModel.json

The ExternalRequestModel.json file defines the JSON schema of the ExternalRequestModel and has the following content:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "description": {
      "type": "string"
    },
    "name": {
      "type": "string"
    }
  }
}

The ExternalResponseModel.json file defines the JSON schema of the ExternalResponseModel and has the following content:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "status": {
      "type": "string"
    }
  }
}

The file openapi.yml contains the OAS and references for the POST request the external JSON schema files ExternalResponseModel.json and ExternalResponseModel.json via the external references $ref: 'model/ExternalRequestModel.json' and $ref: 'model/ExternalResponseModel.json'.

openapi: 3.1.0
info:
  title: Example 3 - OAS
  version: 1.0.0
paths:
  /endpoint:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: 'model/ExternalRequestModel.json'
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: 'model/ExternalResponseModel.json'

Model Validations

JSON is commonly used when transferring data using REST. Models usually have properties or named fields that can be simple or complex data types.

Actually always the presence and the content conformance of the model properties is necessary for the subsequent processing. If the value is not present or out of an expected range, error handling must be done in the application logic. Validation rules help to define preconditions in models and to check them using validators before the request reaches the application logic.

For example, a property of a model can be defined as required. Furthermore, a property of a model can be assigned a data type (e.g. string, integer or boolean). This ensures that no string value can be sent or received for a property defined as an integer.

It is possible to further restrict simple data types. For string based text values, for example, the following restrictions can be set with JSON Schema to further describe the data:

  • minLength: the string needs to have at least minLength characters
  • maxLength: the string does not have more than maxLength characters
  • pattern: the string needs to follow the defined regular expression (RegEx)

Numeric values can be specified using the following restrictions:

  • minimum: the number needs to be greater or equal the minimum
  • maximum: the number needs to be smaller or equal the maximum
  • multipleOf: the number needs to be a multiple of a value

In the standard, the ranges are inclusive, but can also be described as exclusive via the boolean property exclusiveMaximum.

The conformity check is done before the actual application logic. Since no invalid request reaches the application logic, no error handling is needed there. The application logic can thus be reduced to the actual processing.

OpenAPI Code Generator

Both streams described above allow code to be generated from the specification. Since in the code-first (stub only) approach the specification is derived from the server implementation, the scope of generation is limited to the client adapter and its models. In contrast, when using the contract-first (stub and skeleton) approach, it is possible to derive both the server framework (skeleton) and client adapter (stub) with the models from the specification.

Inputs and Outputs of the Generator

Installation

This example uses the openapi-generator of the OAI for code generation. The generator can be installed and started in different ways, for example:

  • started as CLI tool
    • npm: npm install @openapitools/openapi-generator-cli -g
    • homebrew: brew install openapi-generator
    • docker: docker pull openapitools/openapi-generator-cli
  • integrated in the build tool
    • maven: org.openapitools:openapi-generator-maven-plugin
    • gradle: org.openapitools:openapi-generator-gradle-plugin

Modules and Libraries

The generator follows a modular structure to support different programming languages for both client and server code generation.

Modules and Libraries of the Generator

At the time of writing this article, there are 69 modules for client generation and 57 modules for server generation documented. Furthermore generator modules are subdivided into libraries to adapt the modules to the respective development environment.

Example: Distributed Hello World Application

First, the OAS of the client-server application is described. Then the general configuration of the maven plugin openapi-generator-maven is shown. Afterwards the server application is considered. The maven configuration, the generated classes and the generated interface implementing controller of the server are explained. Subsequently, the client application is examined. Analogous to the server application, the generated classes and the maven configuration are shown. Instead of the controller implementation the call of the API is finally described.

Common Elements

The following subchapters describe common features of the client and server applications. The specific configurations are described in the descriptions of the respective applications.

HelloWorld Contract

The distributed HelloWorld application uses the following OAS with embedded models in the components section.

Note that - in extension of the previous examples - the fields tags and operationId are added to the POST request specified at the /helloworld path The first tag in the field tags is used to generate the class names and the operationId is used to generate the method names of the generated class.

openapi: 3.1.0
info:
  title: Example 4 - HelloWorld API
  version: 1.0.0
paths:
  /helloworld:
    post:
      operationId: helloWorld
      tags:
        - helloWorld
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HelloRequest'
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HelloResponse'
components:
  schemas:
    HelloRequest:
      type: object
      properties:
        name:
          type: string
    HelloResponse:
      type: object
      properties:
        greeting:
          type: string

OpenAPI Generator Maven Plugin

In the example the the openapi-generator maven build plugin org.openapitools:openapi-generator-maven-plugin in version 6.4.0 is used to generate the client as well as the server code.

The plugin is included in the maven pom.xml as </build> plugin. With the help of </execution> element in the </executions> element several differently configured executions of the generator can be defined. For example to generate different clients or servers interfaces from different specifications:

<build>
    <plugins>
        <plugin>
            <groupId>org.openapitools</groupId>
            <artifactId>openapi-generator-maven-plugin</artifactId>
            <version>6.4.0</version>
            <executions>

	            <!-- the execution elements -->

            </executions>
        </plugin>
    </plugins>
</build>

The following example shows the general frame of the </execution> element stored in the </executions> element. The tag </goals> defined in the </execution> element can hold multiple </goal> elements. In the example the </goal> element is defined to call the generate target of the plugin org.openapitools:openapi-generator-maven-plugin on execution. Besides the generate goal the maven plugin supports a goal called help to display plugin information.

Further the </execution> element consists of general generator settings that are set in the </configuration> element. In the </configuration> element the generator module specific settings are set in the tag </configOptions>:

<execution>
    <goals>
        <goal>generate</goal>
    </goals>
    <configuration>
    	
    	<!-- the general configuration -->
      	
       <configOptions>
       
           <!-- the generator module configuration -->
    	
       </configOptions>
    </configuration>
</execution>

In the general generator settings in the </configuration> element, the following important parameters are located:

  • </inputSpec>: path or URL to the OAS
  • </generatorName>: the generator module to use
  • </output>: the output folder of the generator
  • </verbose>: Allows to print out debug information

Other possible configuration values are described in the generator configuration documentation.

Furthermore, in the </configOptions> element of the </configuration> element
the following common for both the Client and Server Java applications:

  • </apiPackage>: package path for the API classes
  • </modelPackage>: package path for model classes
  • </library>: library used depending on the used generator module
  • </useTags>: Defines if tags of the OAS are used for naming the generated classes
  • </useJakartaEe>: Defines if javax.* or jakarta.* namespace for Java EE is used.
  • </openApiNullable>: Defines if OpenAPI nullable annotations are used.

Hint: Tags are useful for naming the generated classes. If the </useTags> element is set to true, the generator will create the name of the class based on the first listed tag and will also add the suffix Api (here: HelloWorldApi.class). If no tag is set, or </useTags> is set to false the class name is set to DefaultApi.class.

To generate the client classes on one side and the server adapter on the other, the following combination of generator modules and libraries is used:

openapi-generator
├── server
│   └── spring (module)
│       └── spring-boot (library)
└── client
    └── java (module)
        └── resttemplate (library)

Further information on the modules used can be found in the documentation for the java and spring modules.

Dependencies

This chapter lists the dependencies used by both the client and the server. The dependencies are due to the generation of the classes and interfaces. Especially in relation to the serialization functionalities of the models and the automatically generated test frameworks.

The openapi-generator generates the Java POJOs out of the OAS compatible to the jackson, jsonb or gson JSON to Java POJO serialization library. Our classes derived from the OAS are created with Jackson annotations. For this reason our application has dependencies to com.fasterxml.jackson.core:jackson-core:2.14.2 and com.fasterxml.jackson.core:jackson-databind:2.14.2.

Jackson is a package of data processing tools for Java. It consists of the streaming, annotations and databind components. The Jackson annotations component provides @Interfaces that can be used to annotate Java POJOs. The annotations provide serialization and deserialization control. Common Java annotations are: @JsonIgnore, @JsonIgnoreProperties, @JsonIgnoreType, and @JsonAutoDetect. The serialization library can be selected via the </configOptions> parameter </serializationLibrary>.

For our generated classes and interfaces, frames of test classes are automatically generated by the generator. At the current time only JUnit 4 is supported. For this reason the following dependency is required: junit:junit:4.5.

Since both applications are based on SpringBoot, both applications depend on the SpringBoot parent and have org.springframework.boot:spring-boot-starter-parent:3.0.2 as dependency.

Applications

The following chapter describes the implementations of client and server applications using SpringBoot in a contract-first approach. The underlying communication structure can be described as follows:

Client-Server IPC

The structure of the chapters to describe the client and server application is kept identical. First, the project structure is presented. This is followed by a description of the respective Maven pom.xml and the dependencies used. Afterwards the generated classes and interfaces are explained. Finally, the respective application code is explained.

Server application

First the project structure and the maven configuration is described. Then the generated files (server interfaces and model classes) are explained. Finally the generated classes are used exemplarily in a main method providing a SpringBoot Controller.

Project Structure

Our server application keeps the Maven pom.xml in the Project Folder HelloWorldServer. The path src/main/resources/ contains the OAS with the name openapi.yml. The self-written class StartServer.java is stored in the path src/main/java in the package org.example.server. The class contains the main method of the SpringBoot Web Application and a SpringBoot Controller implementing the OAS generated interface. The generated classes are stored by the generator in the target/generated-sources path.

HelloWorldServer
├── src
│   └── main
│       ├── java
│       │   └── org.example.server
│       │       └── StartServer.java
│       └── resources
│           └── openapi.yml
├── target
│   └── generated-sources
└── pom.xml

Maven pom.xml

In addition to the common ones already mentioned, further dependencies are required in the server application. The server application is a SpringBoot Web application exposing a REST Controller and therefore needs the org.springframework.boot:spring-boot-starter-web as dependency. The dependency org.springframework.boot:spring-boot-starter-validation is necessary to use Validation in REST Controllers based on generated model annotations.

In addition to the configuration elements of the openapi-generator-maven-plugin plugin already described, the following additional configOptions parameters are set for the generator module spring for server generation. A complete overview of all available options of the generator spring can be found in the documentation.

  • </documentationProvider>: Field to select documentation provider (e.g: none, source, springfox or springdoc)
  • </skipDefaultInterface>: Defines if default implementation of the java interface is created, returning status code 501 Not Implemented
  • </requestMappingMode>: Defines the location of the @RequestMapping annotations (e.g: api_interface, controller or none)
  • </useSpringBoot3>: Defines if code is generated with Spring Boot Version 3 dependencies (also enables </useJakartaEe>)
  • </interfaceOnly>: Defines if only API interfaces are generated.
  • </useSpringController>: Defines if @Controller is added to the generated Interface.

The pom.xml used for the server application has the following content:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example.api</groupId>
    <artifactId>server</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>19</maven.compiler.source>
        <maven.compiler.target>19</maven.compiler.target>
    </properties>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.2</version>
    </parent>
    <dependencies>
         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.14.2</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.14.2</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.5</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.openapitools</groupId>
                <artifactId>openapi-generator-maven-plugin</artifactId>
                <version>6.4.0</version>
                <executions>
                    <execution>
                        <id>generate-server</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <inputSpec>${project.basedir}/src/main/resources/openapi.yml</inputSpec>
                            <generatorName>spring</generatorName>
                            <output>${project.build.directory}/generated-sources/openapi-server</output>
                            <verbose>true</verbose>

                            <configOptions>
                                <apiPackage>org.example.api.server</apiPackage>
                                <modelPackage>org.example.api.models</modelPackage>
                                <library>spring-boot</library>
                                <useTags>true</useTags>
                                <useJakartaEe>true</useJakartaEe>
                                <openApiNullable>false</openApiNullable>
                                <documentationProvider>none</documentationProvider>
                                <skipDefaultInterface>true</skipDefaultInterface>
                                <requestMappingMode>api_interface</requestMappingMode>
                                <useSpringBoot3>true</useSpringBoot3>
                                <interfaceOnly>true</interfaceOnly>
                                <useSpringController>true</useSpringController>
                            </configOptions>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Generated classes

When running mvn compile, the openapi-generator-maven-plugin creates the server classes in the configured </output> directory (here:${project.build.directory}/generated-sources/openapi-server). Beside further helper classes the following primary .java files are generated:

HelloWorldServer
├── src
│   └── main
│       ├── java
│       └── resources
├── target
│   └── generated-sources
│       └── openapi-server
│           └── src
│               └── main
│                   └── java
│                       └── org.example.api
│                           ├── server
│                           │   └── HelloWorldApi.java 
│                           └── models
│                               ├── HelloRequest.java
│                               └── HelloResponse.java
└── pom.xml

In the files HelloRequest.java and HelloResponse.java the JSON models defined in the OAS - #/components/schemas/HelloRequest and #/components/schemas/HelloResponse were converted into Java classes.

The file HelloWorldApi.java is the actual API interface. It describes the servers functionalities and needs to be implemented in self written SpringBoot class annotated with @Controller or even @Service.

Server application code

The Server application class is defined in the file StartServer.java. The class is annotated with the @SpringBootApplication annotation. A static final String field GREETING with a String format pattern "Hello %s" is added and will be used to act as business logic to convert the name stored in the HelloRequest instance to an HelloResponse instance holding a greeting String.

The entry point of the application is the method public static void main(String[] args). In the method the SpringBoot Web application is started with the call SpringApplication.run(StartServer.class, args).

Inside the StartServer.java file an inner class HelloWorldController annotated with the @Controller annotation. The HelloWorldController is implementing the generated interface HelloWorldApi and its methods. To be compliant with the HelloWorldApi java interface, the method ResponseEntity<HelloResponse> helloWorld(HelloRequest helloRequest) is implemented.

Inside the helloWorld method, first the greeting String is generated using the format pattern GREETING and the name out of the HelloRequest with the call greeting = String.format(GREETING, helloRequest.getName()) Then a new HelloResponse instance is created and the greeting is set to the response. Finally a new ResponseEntity holding the HelloResponse instance is returned.

@SpringBootApplication
public class StartServer{

    private static final String GREETING = "Hello %s";

    public static void main(String[] args) {

        SpringApplication.run(StartServer.class, args);

    }

    @Controller
    public class HelloWorldController implements HelloWorldApi {

        @Override
        public ResponseEntity<HelloResponse> helloWorld(final HelloRequest helloRequest) {

            final String greeting = String.format(GREETING, helloRequest.getName());

            final HelloResponse helloResponse = new HelloResponse();
            helloResponse.setGreeting(greeting);

            return ResponseEntity.ok(helloResponse);

        }
    }
}

Client application

First the project structure and the maven configuration is described. Then the generated files (client classes and model classes) are explained. Finally the generated classes are used exemplarily in a main method.

Project Structure

Our client application keeps the Maven pom.xml in the Project Folder HelloWorldClient. The path src/main/resources/ contains the OAS with the name openapi.yml. The self-written class StartClient.java is stored in the path src/main/java in the package org.example.client. The class contains the main method of the SpringBoot Console Application and calls the generated API client. The generated classes are stored by the generator in the target/generated-sources path.

HelloWorldClient
├── src
│   └── main
│       ├── java
│       │   └── org.example.client
│       │       └── StartClient.java
│       └── resources
│           └── openapi.yml
├── target
│   └── generated-sources
└── pom.xml

Maven pom.xml

In addition to the common ones already mentioned, further dependencies are required in the client application. The client application is a SpringBoot CLI application and therefore needs the org.springframework.boot:spring-boot-starter as dependency. The dependency org.springframework:spring-web is necessary to use the HTTP functionalities of Spring (e.g. RestTemplate) in our application.

The pom.xml used for the client application has the following content:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example.api</groupId>
    <artifactId>client</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>19</maven.compiler.source>
        <maven.compiler.target>19</maven.compiler.target>
    </properties>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.2</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.14.2</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.14.2</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.5</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.openapitools</groupId>
                <artifactId>openapi-generator-maven-plugin</artifactId>
                <version>6.4.0</version>
                <executions>
                    <execution>
                        <id>generate-client</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                         <configuration>
                            <inputSpec>${project.basedir}/src/main/resources/openapi.yml</inputSpec>
                            <generatorName>java</generatorName>
                            <output>${project.build.directory}/generated-sources/openapi-client</output>
                            <verbose>true</verbose>
                            <configOptions>
                                <apiPackage>org.example.api.client</apiPackage>
                                <modelPackage>org.example.api.models</modelPackage>
                                <library>resttemplate</library>
                                <useTags>true</useTags>
                                <useJakartaEe>true</useJakartaEe>
                                <openApiNullable>false</openApiNullable>
                            </configOptions>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

When generating the client with the generator module java other http implementations can be used besides resttemplate. Examples are the http implementations feign, webclient the native java client or the apache-httpclient.

Generated classes

When running mvn compile, the openapi-generator-maven-plugin creates the client classes in the configured </output> directory (here:${project.build.directory}/generated-sources/openapi-client). Beside further helper classes as well as additional support files the following primary .java files are generated:

HelloWorldClient
├── src
│   └── main
│       ├── java
│       └── resources
├── target
│   └── generated-sources
│       └── openapi-client
│           └── src
│               └── main
│                   └── java
│                       └── org.example.api
│                           ├── client
│                           │   └── HelloWorldApi.java
│                           ├── models
│                           │   ├── HelloRequest.java
│                           │   └── HelloResponse.java
│                           └── ApiClient.java
└── pom.xml

In the files HelloRequest.java and HelloResponse.java the JSON models defined in the OAS - #/components/schemas/HelloRequest and #/components/schemas/HelloResponse were converted into Java classes.

The file ApiClient.java is the wrapper class for the library (here: RestTemplate) configured in the openapi-generator-maven-plugin configuration.

The ApiClient class can be instantiated in two ways:

  1. With the default constructor: apiClient = new ApiClient()
  2. By passing the HTTP client implementation configured via library: apiClient = new ApiClient(restTemplate)

If the default constructor is called (variant 1), the configured HTTP client implementation is created in the wrapper. Passing the HTTP client (variant 2) can be useful if an instance of the HTTP client implementation - preconfigured by the context - is to be used in the ApiClient instance.

The wrapper allows setting the basePath (e.g: apiClient.setBasePath("http://localhost:8080")). In case of authentication defined in the OAS, values to be used for username, password or token can be set in the ApiClient instance. Furthermore, additional header parameters can be added (e.g: apiClient.addDefaultHeader("KEY", "VALUE")).

The file HelloWorldApi.java is the actual API class. It encapsulates the call to the server implementation.

The HelloWorldApi class can be instantiated in two ways:

  1. With the default constructor: helloWorldApi = new HelloWorldApi()
  2. By passing the ApiClient instance: helloWorldApi = new HelloWorldApi(apiClient)

If the default constructor is called (variant 1), a new ApiClient is created in the wrapper. The ApiClient is returned from the HelloWorldApi by the following call helloWorldApi.getApiClient(). Passing a configured ApiClient (variant 2) can be useful if, for example, a preconfigured ApiClient is to be used with a context-preconfigured client of an HTTP client implementation in the ApiClient.

Client application code

The client application class is defined in the file StartClient.java. The class is annotated with the @SpringBootApplication annotation and implements the CommandLineRunner interface. This requires implementing the void run(String... args) method.

The entry point of the application is the method public static void main(String[] args). In the method the SpringBoot CLI application is started with the call SpringApplication.run(StartClient.class, args).

The request from the client to the server is made in the implemented method void run(String... args). First a RestTemplate is initialized, which is used when initializing the ApiClient. Then the basePath of the server is set for the instance of the ApiClient. If the instance of the ApiClient is configured, an instance of the HelloWorldApi is created by passing the instance of the ApiClient.

After preparing the HelloWorldApi a HelloRequest instance is created and the model parameter name is set. Now the actual API call to the server is made. The instantiated request HelloRequest is passed to the helloWorld() method of HelloWorldApi. On a successful call, an instance of the HelloResponse class is returned. Finally, the greeting can be read from the HelloResponse with the getter getGreeting().

@SpringBootApplication
public class StartClient implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(StartClient.class, args);
    }

    @Override
    public void run(String... args) {

        final RestTemplate restTemplate = new RestTemplate();
        
        final ApiClient apiClient = new ApiClient(restTemplate);
        apiClient.setBasePath("http://localhost:8080");

        final HelloWorldApi helloWorldApi = new HelloWorldApi(apiClient);

        final HelloRequest helloRequest = new HelloRequest();
        helloRequest.setName("John Doe");

        final HelloResponse helloResponse =  helloWorldApi.helloWorld(helloRequest);
        
        System.out.println(helloResponse.getGreeting());

    }
}

Conclusion

The contract-first approach has several advantages over the code-first approach. For example, the coupling between client and server applications can be eliminated via the OAS. The coupling with regard to the development teams is also positively influenced since they can develop their applications simultaneously. Furthermore, the specification of contracts reduces risks of integration failures. Through the OAS, API design and functionality are defined unambiguously for humans and machines and without contradictions. The generated abstraction hides the underlying complexity and implementation details.

For server-side developers, the focus shifts from integration details to developing the value-creating application logic. The developer has interface descriptions, defined models for the input values (requests) and the output values (responses). Only the conversion of the input data to the output data, using calculations in the processing or business logic, remains to be implemented.

On the client side, the capabilities of the server can be used directly after the client code has been generated. The focus is also shifted from integration to embedding the generated code in the consumer’s context. It is only necessary to instantiate the existing models of the requests with values from the consumer’s context and pass them to the available API client code. Finally, after a call to the API client code, the obtained values of the existing response models can be used immediately in the consumer’s context and create value.

Thanks to SpringBoot and the API contracts defined with the OAS, client and server applications can be developed in a short time.

Happy