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 |
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: 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.
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 leastminLength
charactersmaxLength
: the string does not have more thanmaxLength
characterspattern
: 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 minimummaximum
: the number needs to be smaller or equal the maximummultipleOf
: 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.
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
- integrated in the build tool
Modules and Libraries
The generator follows a modular structure to support different programming languages for both client and server code generation.
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 ifjavax.*
orjakarta.*
namespace for Java EE is used.</openApiNullable>
: Defines if OpenAPInullable
annotations are used.
Hint: Tags are useful for naming the generated classes. If the
</useTags>
element is set totrue
, the generator will create the name of the class based on the first listed tag and will also add the suffixApi
(here:HelloWorldApi.class
). If no tag is set, or</useTags>
is set tofalse
the class name is set toDefaultApi.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:
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
orspringdoc
)</skipDefaultInterface>
: Defines if default implementation of the java interface is created, returning status code501
Not Implemented</requestMappingMode>
: Defines the location of the@RequestMapping
annotations (e.g:api_interface
,controller
ornone
)</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:
- With the default constructor:
apiClient = new ApiClient()
- 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:
- With the default constructor:
helloWorldApi = new HelloWorldApi()
- 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.