How to write robust REST API with OpenAPI
Written by Yonatan Karp-Rudin
While working as a backend engineer, I developed a few REST APIs. One thing that always happened to me (no matter how simple or complicated the API was) is that the integration between the backend and the clients has never been smooth. A typo in the URL, using camel case field name in the JSON instead of snake case, passing value as a string instead of an integer, and many more examples have happened to us.
2 years ago, my team and I had to design a new API that will be integrated by multiple clients: mobile (Android & iPhone), web, and other backend services. We wanted to make the integration as painless as possible, and the API definition as robust as possible. During our brainstorming about the design of the API, we decided to go with OpenAPI (previously known as Swagger). I knew Swagger before, but only as a documentation tool for already written code. This time, we decided to build the API the other way around. We started by defining the spec with all the endpoints, requests & responses, and then, each integrator of the spec (backend, mobile, and web) has auto-generated the models, clients, or controllers and used them in the codebase, so there was no room for mistakes.
The design was a big success, and we've replicated the same behavior again and again for new APIs to come. More and more teams started to adopt OpenAPI for this exact purpose. Actually, by now, it became such a big success that the company decided to adopt OpenAPI as the official way to describe all of our Apis for integration inside and outside the company.
In this article, I will show a short and simple API example and the process we did to have a working REST API. In our tech stack, we will use SpringBoot as the framework, Kotlin as the language, and Gradle's Kotlin DSL as the build system - but please note, OpenAPI supports many different languages and I just decided on this one. For the full list, you can check this link
We want to design a simple API. Our API will get a name on the endpoint /greet and a name to greet as the query parameter (e.g. /greet&name=Yonatan). We will respond with the answer hello + $name.
Let's start by defining our spec! I would highly encourage you to use the Swagger Editor to detect syntax errors in your spec in advance. If you're using IntelliJ IDEA, you can also use the OpenAPI Editor plugin, which works extremely well.
We will use this definition:
Let's look at the spec step by step and understand each section.
In the info section, we define the information that will appear on our documentation, alongside the API version, the owners of the API, etc. You can find more attributes to add to this section at API General Info.
Servers This list of servers will allow the people who use the documentation made from the spec to select the environment they want to use (e.g. local, dev, staging, and production). For example:
The endpoints Under the path section, we will define all the different endpoints that would be part of our API, their HTTP methods, request & responses.
The models In the components section, are defining the different models that would be part of our API. In our case, we simply define a response for the endpoint that will contain a single field (that cannot be null as it's marked as required). This section can contain more information such as the security schema (e.g. which authentication method are we using in our API, general headers that are required, and more)
First, let's store our spec into our project, we can create an api directory at the project root, and store it into a spec.yml file. Your project should look like this:
The next step would be to add a config.json file to our api directory that will include the different flags that we would like to set. Let's create that file with the following content:
You can find all the different available flags that you can set here in the generator documentation.
Your project should look now like this:
Now, let's start by adding the plugin to our project. Open your build.gradle.kt and add the following to your plugins:
Now, finally, let's configure the Gradle plugin to work with all the things we've set! We will build our models into the build directory on each build of our project, so we will not need to commit any auto-generated code to our project.
Note, it is possible to override the templates coming from the generator in case you need to add/remove something from the generated classes. To do so, copy the templates you need from the generator repository, amend them, and set the files in the plugin with the following line:
For more information about the plugin configurations click here
I would recommend adding the following code that will make sure that every time we're using the clean command our generated code would be cleaned as well, and that each build will be deepened on the generated code to exist.
Last but not least, let's make sure that all of our code is included in our source set:
If you'll run the build command now in Gradle you should see something similar in your project files:
As you can see, the code currently has errors as it cannot find the javax.validation package. we can easily solve it by adding to our build.gradle.kt the following dependency:
This stage is the easiest step so far. We will implement our generated interface GreetingApi in our controller. Let's write the code!
We can now run our server and try to call it from the browser:
Let's add a few tests to our API! Since we have no actual business logic here, I would have only an integration test to our new code that will check the flow completely.
Lets start by defining our WebConfig class in our src/main/kotlin directory with the following content:
Now, we can add our test class. I would use the @ParameterizedTest functionality of jUnit5 to avoid code duplications. Let's look at the code:
If you did everything correctly, by running the test you should see that you have 2 green tests! 🎉
The finished project is also available on my GitHub repository so you can see the finished code there. you can visit by clicking here.Back to our tech stories