The REST API implementation flow
Creating and sharing a contract for an API between the backend and the clients is one of the first steps of a feature development. There are many ways of doing this from just agreeing in speech to creating OpenAPI descriptors on the spot. In this article I will show a way of doing this process that I found very efficient and practical.
Designing an API
In the best possible scenario all the affected teams will be present in a discussion where the API is designed.
- This way every party have a saying in what endpoints to create to be able to satisfy the requirements
- Various platforms can have different preferences on implementation details. For e.g.: returning nulls in JSON or not returning the given key at all? If there are no company policies or long used conventions about these it is best if all platforms are present when the API is designed.
I found that the best is to start an OpenAPI descriptor file right at the meeting. It is not that difficult to write in your favourite text editor by using an example. You can also use some kind of a designer for it like the online Swagger editor or any other GUI tool. I have some experience with Stoplight Studio from a year ago, but it had some issues and I found writing the code to be easier than playing with a GUI. Obviously we do not want to waste everyone's time on a meeting with figuring out the syntax so a little practice beforehand can help a lot. Also it is not a goal on the meeting to create the final fully complete version. A draft is enough and after some polishing it can be shown to the others for a final approve.
Implementing the software
At this point we should have an OpenAPI descriptor agreed upon by all the devs, but where should this be stored? In my experience a lot of time it is up to the backend devs to come up with the API, handle the infra and along these lines, to write and manage the API descriptors too. In these scenarios the OpenAPI files are usually part of the backend service repository. This works fine, but there are better ways.
We established that coming up with the APIs should be a team effort rather than something the backend team does. Once the design is done we can put the resulting yaml file(s) to their own repository. In many cases this same repository held Docker compose files, Kubernetes descriptors and other configuration related to the infrastructure that run the software, hence the repo was called infra, storing everything not specific to any of the platforms (backend, web, mobile, etc).
Okay so we have a repository hosting our files. Why bother writing those fancy yaml descriptors if there is no use from it? Well there can be. For years I used to write all the API controllers and DTOs by hand based on the API we decided upon, but once I tried code generation there was no way back.
The OpenAPI generator project aims to create code generation tools for OpenAPI descriptors for a lot of languages and platforms. In this topic I will only touch the Java related ones, but check out their list of supported platforms, there is a good chance it covers yours. (There are several generators available, another popular one is NSwag for example.)
Obviously to generate code you will need to have the descriptors! Probably the easiest way to do this is to add the infrastructure repository as a git submodule to all the others, so those can directly access the necessary files. This adds a bit complexity, but nothing impossible to handle. This way the descriptor is still managed centrally, everyone gets the updates and it is still present in every project and can be used for code generation.
OpenAPI code generation for the backend
Code generation is super easy. From an OpenAPI descriptor yaml (or json), we can generate client and server side code, so both sides can be handled.
In the example I will use the Swagger pet store example API descriptor and another one for a free to use UUID generator API. My example code will implement parts of the pet store API, so the code generation will generate server side for this, and my code will call the UUID generator service so for that I will generate client side code.
First steps
There is an OpenAPI generator gradle plugin that we can use to create the generated code.
plugins {
id 'org.springframework.boot' version '2.7.6'
id 'io.spring.dependency-management' version '1.1.0'
id 'java'
// applying the openapi generator plugin
id "org.openapi.generator" version "6.2.1"
}
We can and should also add generated code folders to the source sets, so we can refer to these classes and methods from the code we write.
// adding the generated sources to the source sets
sourceSets.main.java.srcDirs += 'build/generated/petstore/src/main/java'
sourceSets.main.java.srcDirs += 'build/generated/uuid/src/main/java'
Server code
Let's start with the server code. So we have a petstore.yml
file describing the API we have to implement for our clients. To make this easier we will have to generate some server side code.
The OpenAPI generator project has many generators, not just for languages but for specific frameworks and platforms too. Luckily they have a very good one for Spring too! Another nice thing is that these generators are insanely fine tuneable. You can look up the documentation for any, but here is the one for the Spring generator. The following config from my example are some sensible defaults I usually start with, but is worth reading through the docs as there are many features that may come handy.
task buildPetStoreServer(type: GenerateTask) {
generatorName = "spring"
groupId = "$project.group"
id = "$project.name-java-client"
version = "$project.version"
inputSpec = "$rootDir/petstore.yml".toString()
outputDir = "$buildDir/generated/petstore".toString()
apiPackage = "com.tmsvr.openapidemo.petstore.api"
invokerPackage = "com.tmsvr.openapidemo.petstore.invoker"
modelPackage = "com.tmsvr.openapidemo.petstore.model"
generateApiDocumentation = false
generateModelDocumentation = false
generateModelTests = false
generateApiTests = false
configOptions = [
java8 : "true",
dateLibrary : "java8",
serializationLibrary: "jackson",
library : "spring-boot",
useBeanValidation : "true",
interfaceOnly : "true",
serializableModel : "true",
useTags : "true"
]
}
The task we just defined should be added to the build script so it will automatically executed once e build the project. It can be done like this.
compileJava.dependsOn tasks.buildPetStoreServer, tasks.buildUuidClient
Gradle will put the generated code under build/generated (this can also be configured).
As you can se the plugin basically creates a complete project for us with a pom.xml and all that is needed, the readme even tells us how to use it. The controllers are created as interfaces that we have to implement. These are not always the nicest code, but the good part is that we don't really have to look at them. Below is an example from the generated PetApi
interface.
There is a nice default implementation based on the examples provided in the descriptor which can be used if deployed to an environment to provide a test backend to the client developers asap. Using these default implementations takes off the pressure from the backend team as they won't be blocking the client developers. There are also OpenAPI compatible mock servers out there that can be used for this purpose, but I don't really have experience with those.
If you have to change something about the generated code that is not configurable it is also relatively easy to do so. The templates that the generator uses are simple mustache files. The documentation tells you how you can overwrite these if you want to polish the generated code, but I never faced a situation where this was needed.
Now all we have to do is to implement this interface in our code and override the default methods.
@Slf4j
@RestController
@RequiredArgsConstructor
public class PetStoreController implements PetApi {
private final UuidService uuidService;
@Override
public ResponseEntity<Pet> getPetById(Long petId) {
log.info("FOUND");
List<String> uuids = uuidService.getUuids();
for (String uuid : uuids) {
log.info("UUID: {}", uuid);
}
var foundPet = new Pet();
foundPet.setId(1L);
foundPet.setName("Beethoven - " + uuids.get(0));
return ResponseEntity.ok(foundPet);
}
// ...
}
Client code
Client code can be similarly generated. The config I usually start with is below, but it obviously depends on the platform/framework you are working with. This one is a good starting point for Spring Boot projects.
task buildUuidClient(type: GenerateTask) {
generatorName = "java"
groupId = "com.tmsvr.api"
id = "uuidapi"
inputSpec = "$rootDir/uuid.yml".toString()
outputDir = "$buildDir/generated/uuid".toString()
apiPackage = "com.tmsvr.openapidemo.uuid.api"
invokerPackage = "com.tmsvr.openapidemo.uuid.invoker"
modelPackage = "com.tmsvr.openapidemo.uuid.model"
configOptions = [
dateLibrary: "java8",
library: "resttemplate",
serializableModel : "true",
hideGenerationTimestamp : "true"
]
}
The generated UuidApi
is the class you have to use in your code. It can be simply injected and is ready to use.
@Slf4j
@Service
@RequiredArgsConstructor
public class UuidService {
private final UuidApi uuidApi;
public List<String> getUuids() {
try {
return uuidApi.generateUuid(4);
} catch (RestClientException restClientException) {
// TODO handle exceptions here
log.warn(restClientException.getMessage(), restClientException);
return List.of("not-so-unique");
}
}
}
Client code generation is a good way to generate and provide a client lib / SDK to your API users. Client applications like web and mobile will also use the client type generators.
Summary
API design is a team effort. Coming up with an OpenAPI descriptor for the API designs provides many advantages early on and during the development.
- All parties have an interface contract they can refer to
- A mock backend can be set up very quickly to support client developers
- Code generation can save a lot of work
- A company or team adopting these techniques and building processes and CI/CD around these ideas and tools can really improve on efficiency and speed gaining an advantage on the market
The complete example project can be found in my git repo below.