Java: Developing a Spring service using an OpenAPI Contract First approach

Joe Honour
9 min readJul 26, 2020

Tech stack: Java 14, OpenAPI 3, Spring, Docker, Gradle

Figure 1: using the OpenAPI specification to provide consumers with a known REST interface to our service, while also using the same specification to generate our service interface classes.

A contract first approach, in the realm of REST APIs, is the methodology of developing a specification before you begin implementing your service. In an initial attempt at this, what often happens is you write an OpenAPI (or Swagger) document, give this to your consumers, then go about implementing your service against this specification. This has some great benefits:

  1. your consumers can build their services against your specification, while you are still implementing the service.
  2. your consumers can test their service conforms to your contract when making requests, and their service behaves correctly under expected responses.
  3. both you, and your consumers, work to the same formal and verifiable contract. This helps eliminate integration pain points and ambiguity when the services finally speak to each other “in the real”.

However, even with these benefits, we still have one major drawback.

How do we guarantee the service we implemented, conforms to the contract we gave everyone?

This is usually where things get ‘interesting’. We often realise we need new fields, or slightly different endpoints, to provide a service than originally thought. You end up having to go back to your specification, after you finished implementing the service, and make tweaks to make it match what you actually implemented. This can effect your consumers and cause pain points as you integrate, especially if these discrepancies are only noticed late into the development lifecycle. So how can we avoid this ?

A Contract-first approach: we always update our public contract before we begin changing the implementation of a service, making the contract the single source of truth for a services interface. To guarantee the service never deviates from the contract, we can generate the interface code from the contract. This stops any manual changes from being able to creep into the service implementation, that would contradict the public contract.

In this guide, we will begin by writing an OpenAPI specification. However, instead of just giving this to our consumers, we will also generate our Spring models and controllers from it (Figure 2). This means, the only way to change the interface to our service is to change the contract. If we realise we need to change the contract, we can notify consumers much earlier to the change, or adapt other techniques to ease with integration (such as versioning our API).

If you would like to skip this guide, and just see the finished sample code, you can find that here.

Figure 2: the delivery lifecycle of a change to your service. You must, change the OpenAPI spec, on build the new spring configuration is generated conforming to the spec, finally you implement any changes needed within your application.

Technologies

To build our contract-first service, we will be using the following technology stack:

  • Java 14: latest and greatest version of Java at the time of writing (though you can adjust the version of Java needed for your application).
  • OpenAPI: we will use the OpenAPI 3 specification to write a contract for a simple REST application, which we can then use to generate a Spring interface.
  • OpenAPI Generator: we will use a gradle plugin to take the OpenAPI specification and generate Spring controllers/models.
  • Spring: the REST API will be created using Spring-Boot (though we will generate nearly all of this from our OpenAPI specification).
  • Gradle: in order to generate, build, and test our Spring application we will make use of gradle tasks.

In order to complete the guide, you will therefore need the following installed:

  1. Java 8 (or above).
  2. IntelliJ (or similar IDE).

The guide also assumes a basic knowledge of Spring, Gradle, and the OpenAPI specification.

Step 1: Generating a base Spring project

To start with, we will generate a base Spring project. Go to the Spring Initialiser website and use the parameters shown below (Figure 3) to create your starting project. We add the spring-web dependency, as this includes the controller/model annotations needed for creating REST APIs. Along with this, we include the actuator dependency, which configures a /health endpoint (among other things) which we can use to verify the application has started correctly.

Figure 3: parameters needed to generate a starting Spring application.

Once you have this project downloaded, import it into IntelliJ.

Within the project, the major files to look at are:

  • build.gradle: this file is responsible for building and packaging our application. We will extend this later on to add the generation of our Spring controllers/models as a pre-requisites to building the application.
  • HelloserviceApplication.java: this file is responsible for starting our application. It will scan for Spring annotations within its current package, and any sub packages, which we will use when implementing our generated interface.

In order to run the application, you can execute the following command line instruction from the root of the application directory (or use your IDE):

./gradlew bootRun

To verify the application started correctly, open a web browser and go to:

http://localhost:8080/actuator/health

This should return you a status of UP, showing that the application has started successfully.

With this base application structure, we can now look at creating an OpenAPI contract for our application.

Step 2: Creating an OpenAPI specification for the service

For this guide, I will create a simple API with a single endpoint. The endpoint will have the following structure:

  • GET: /hello which returns a simple response of a Hello World string.

Create the following file src/main/resources/service.yaml. This file will be used to store our OpenAPI contract. For our single endpoint, the following specification can be used:

openapi: "3.0.0"
info:
version: 1.0.0
title: Hello World!
servers:
- url: http://localhost:8080
paths:
/hello:
get:
summary: Gets Hello World
operationId: hello
tags:
- hello
responses:
'200':
description: Hello World
content:
application/json:
schema:
$ref: "#/components/schemas/Hello"
components:
schemas:
Hello:
required:
- text
properties:
text:
type: string

The key parts of the specification are:

  • paths: specifies all the endpoints we want our service to offer.
  • /hello: defines a single endpoint, with known request types and responses. In the above specification we support only GET requests, where the response can only contain a 200 (OK) response code, with the response body being built from the Hello schema.
  • components/schemas/Hello: this specifies the properties on a Hello response. You can see there is a single property “text”, of type String, which is required.

With this schema, we can now generate our Spring controllers and models. This will create all Spring config necessary to serve our single GET endpoint with a Hello component response.

Step 3: Generating Spring controllers/models from the specification

In order to generate our Spring classes from the specification, we will be using a gradle plugin.This plugin can take our specification, and with some configuration, generate the Spring classes needed to a specific location. To add the plugin, edit your build.gradle file to contain:

plugins {
id 'org.springframework.boot' version '2.3.2.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'org.openapi.generator' version "4.3.1"
id 'java'
}

group = 'org.guardiandev'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '14'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "io.swagger.parser.v3:swagger-parser:2.0.20"
implementation "org.openapitools:jackson-databind-nullable:0.2.1"

testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}

test {
useJUnitPlatform()
}

The areas in bold contain the additions needed for the OpenAPI generator plugin to be made available:

  • plugins section: applying the plugin provides the gradle runtime with the tasks needed to generate Spring configuration from the specification.
  • dependencies section: the 2 dependencies added are whats needed by the generated files. i.e. the controllers and models rely on having access to the swagger annotation packages in order to compile.

With the OpenAPI generator now available, we can configure it to look at our specification file, and generate the Spring configuration to a known location. In order to do this, extend your build.gradle file with the following configuration.

// generates the spring controller interfaces from openapi spec in src/main/resources/service.yaml
openApiGenerate {
generatorName = "spring"
inputSpec = "$projectDir/src/main/resources/service.yaml"
outputDir = "$buildDir/generated"
apiPackage = "org.guardiandev.helloservice.api"
invokerPackage = "org.guardiandev.helloservice"
modelPackage = "org.guardiandev.helloservice.models"
configOptions = [
dateLibrary: "java8",
interfaceOnly: "true",
]
}

// forces generation of spring controllers on compile, adding them to the sources for compilation
compileJava.dependsOn tasks.openApiGenerate
sourceSets.main.java.srcDir "$buildDir/generated/src/main/java"
sourceSets.main.resources.srcDir "$buildDir/generated/src/main/resources"

The configuration is setup as follows:

  • generatorName: specifies we want to use the Spring generator. The plugin can be configured to generate any number of libraries from the specification.
  • inputSpec: where can we find the OpenAPI spec? Which we configure to be the spec we created in the resources folder.
  • outputDir: where do want these Spring classes to be created? Which we place in the generated folder.
  • apiPackage, invokerPackage, modelPackage: what package names do we want for each respective area that we generate.
  • configOptions: the dateLibrary we want to use when modelling any dates from our specification should be the default Java8 implementation. However, the most notable property here is interfaceOnly. This tells the generator not to create an entire Spring application, we just want it to create the base controllers/models which we can then extend ourselves to implement the logic of how the service works.
  • compileJava.dependsOn: this forces the task from the OpenAPI plugin that generates our Spring configuration, to run immediately before we try and compile our application. This means our generated code is always created, and up to date, during the building of the application.
  • sourceSets: these 2 commands flag to gradle that the generated directory should be available to the application when compiling, otherwise we won’t be able to reference our generated classes when we need to implement the service.

With this configuration added, run the following command to generate your spring controllers and models:

./gradlew clean build

After this runs you should be able to see the following classes created in the directory build/generated/src/main/java/org/guardiandev/helloservice:

  • api/HelloApi.java: the interface with all the Spring annotations needed to provide our Hello endpoint. We will extend this and implement the logic to provide this endpoint in the next step.
  • models/Hello.java: is our simple POJO for serialising our Hello component from the OpenAPI schema we created. You can also see, we have annotations such as @NotNull on the text field, because we marked this property as required in the schema.

This step has generated our Spring configuration from our schema, and our final step is now to extend the HelloApi interface and provide the logic for our service.

Step 4: Writing the service code behind the generated Spring classes

Given we now have our API interface, lets create an implementation of it. To do this, create the following class src/main/java/org/guardiandev/helloservice/controllers/HelloApiController

package org.guardiandev.helloservice.controllers;

import org.guardiandev.helloservice.api.HelloApi;
import org.guardiandev.helloservice.models.Hello;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
@RestController
public final class HelloApiController implements HelloApi {

@Override
public ResponseEntity<Hello> hello() {
return new ResponseEntity<>(new Hello().text("Hello World!"), HttpStatus.OK);
}
}

The above class does the following:

  • it imports our generated model and controller.
  • it extends the generated HelloApi interface, overriding the hello() GET method with our custom implementation.
  • it marks itself as a @RestController so that our application registers this endpoint at startup.

With this class implemented, start your application:

./gradlew clean bootRun

with the application running, go to your browser and visit:

http://localhost:8080/hello

you should see a nice HelloWorld response delivered back from your service. If so, you have completed the guide, and now have a small Spring service delivered with a contract-first approach! :)

In order to fully recognise the benefits of this approach, think about how you might add a new property or endpoint to your service. In order to do this you would have to:

  • update your OpenAPI spec in service.yaml. (contract-first)
  • generate the new models/controllers, by simply building your application again.
  • implement any features needed within your service to support your changes to the specification.

As the contract is always edited first, it means it can never fall behind your service implementation, and you are one step closer to providing a reliable and understood API to your consumers.

Overview and Walkthrough

In this guide, I have tried to sell you on why a contract-first approach is worth investing in:

  • It gives consumers a reliable and accurate interface to build upon.
  • If forces your own implementation of the service to conform to the specification.

While showing a simple approach to adopting this with Gradle and Spring. A full example can be found here, which takes this project further by adding Docker support, providing integration tests, and allows publishing capabilities.

If you have any questions over this approach, feel free to reach out to me.

--

--

Joe Honour

Software Engineer, with interests in Distributed Computing.