Yonatan Karp-Rudin
Yonatan Karp-Rudin

Follow

Yonatan Karp-Rudin

Follow
How to write robust REST API with OpenAPI

Photo by Alvaro Reyes on Unsplash

How to write robust REST API with OpenAPI

Building REST API with OpenAPI, SpringBoot and Kotlin

Yonatan Karp-Rudin's photo
Yonatan Karp-Rudin
ยทDec 23, 2021ยท

8 min read

Play this article

Table of contents

  • Step 1 - Design the API
  • Step 2 - Generating the models
  • Step 3 - Implement the API! ๐ŸŽ‰
  • Step 4 - Writing tests
  • Conclusion

While working as a backend engineer, I developed a few REST APIs. There is one thing that always happened to me. No matter how simple or complicated the API is, that the integration between the backend and the clients has never been smooth. A typo in the URL. using camel instead of snake case in the JSON. Passing value as a string instead of an integer, or many things are great examples that have happened to us.

Frustration

2 years ago, my team and I had to design a new API that integrates with multiple clients. Mobile (Android & iPhone), web, and other backend services. We wanted to make the integration as painless as possible. Moreover, we wanted the API definition to be as robust as possible. During our brainstorming on 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. We defined the requests & responses. Then, each integrator of the spec (backend, mobile, and web) auto-generated the code they used. This code included models, clients, or controllers. And we used them in the codebase, so there was no room for mistakes.

The design was a big success. 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 this point, it became so popular that the company decided to adopt OpenAPI as the official way od describe all our Apis. This allows integration inside and outside the company to easily adopt the API.

In this article, I will show a short and simple API example and the process we did to have a working REST API. For this article, we will use the following stack: SpringBoot, Kotlin, and Gradle's Kotlin DSL. But, OpenAPI supports many different languages and I decided on this one. For the full list, you can check this link.

Step 1 - Design the API

OpenAPI Editor

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 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 well.

We will use this definition:

openapi: 3.0.3

info:
  title: Greeting API
  description: "An API that will send you a greet according to a given name"
  contact:
    name: Yonatan Karp-Rudin
    url: https://yonatankarp.com
  version: 0.1.0

tags:
  - name: Greeting

servers:
  - url: http://localhost:8080/v1
    description: Local development environment

paths:
  /greet:
    get:
      operationId: greet_name
      description: Greeting a given name.
      tags:
        - Greeting

      parameters:
        - in: query
          name: name
          schema:
            type: string
          required: false
          description: The name to greet if no name supplied the API will greet the world

      responses:
        200:
          description: Returning a greeting with the given name.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GreetResponse"

components:
  schemas:
    GreetResponse:
      type: object
      properties:
        greet:
          type: string
          description: The greet from the API.
          example: "Hello, Yonatan!"

      required:
        - greet

Let's look at the spec step by step and understand each section.

API Information

In the info section, we define the information that will appear on our documentation. Moreover, it will include information such as API version, owners, etc. You can find more attributes to add to this section at API General Info.

Servers

The list of servers would allow users to select the environment they want to use when looking at the documentation (e.g. dev). This documentation is automatically made from the spec. For example:

servers:
     - url: https://dev.env
     description: The development environment.
     - url: https://staging.env
     description: The staging environment.
     - url: https://production.env
     description: The production environment.

The endpoints

The path section defines all the different endpoints that are part of our API. It includes their HTTP methods, request & responses.

The models

In the components section, are defining the different models of the API. In our case, we simply define a response for an endpoint that contains a single field. Note that this field cannot be null as it's marked as required. This section can contain more information such as the security schema (e.g. what authentication method are we using? required headers, and more)

Step 2 - Generating the models

To generate our models, we will use OpenAPI Generator. more specifically, we will use the OpenAPI Generator Gradle plugin.

First, let's store the spec in our project. We can create an api directory at the project root, and store it in a spec.yml file. Your project should look like this:

OpenAPI Spec file tree

The next step would be adding a config.json file to our api directory. This config will include the different flags that we would like to set. Let's create that file with the following content:

{
  "interfaceOnly": true,
  "modelPackage": "com.yonatankarp.openapi.models",
  "apiPackage": "com.yonatankarp.openapi",
  "implicitHeaders": true,
  "hideGenerationTimestamp": true,
  "useTags": true
}

You can find all the different available flags of the generator in the documentation.

Your project should look now like this:

project structure with config.json

Now, let's start by adding the plugin to our project. Open your build.gradle.kt and add the following to your plugins:

plugins {
    id("org.openapi.generator") version "5.3.0"
}

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. By doing so, we are not required to commit any auto-generated code to our project.

val apiDirectoryPath = projectDir.absolutePath + File.separator + "api"
val generatedCodeDirectoryPath = buildDir.path + File.separator +
       "generated" + File.separator + "open-api"

openApiGenerate {
    generatorName.set("kotlin-spring")
    inputSpec.set(apiDirectoryPath + File.separator + "spec.yml")
    outputDir.set(generatedCodeDirectoryPath)
    configFile.set(apiDirectoryPath + File.separator + "config.json")
}

Note, it is possible to override the templates coming from the generator. The reason for doing so is in case you need to add/remove something from the generated classes. All you need to do is copy the templates you need from the generator repository. Amend them, and set the files in the plugin with the following line:

openApiGenerate {
    templateDir.set(apiDirectoryPath + File.separator + "templates")
}

For more information about the plugin's configurations click here

I would recommend adding the following code as well. It would ensure that every time we're running the clean command our generated code is cleaned as well. Moreover, we will make the build command dependent on our task. This will guarantee that any new build will generate the newest code from the spec.

tasks {
    register("cleanGeneratedCodeTask") {
        description = "Removes generated Open API code"

        doLast {
            File(generatedCodeDirectoryPath).deleteRecursively()
        }
    }

    clean { dependsOn("cleanGeneratedCodeTask"); finalizedBy(openApiGenerate) }
    compileJava { dependsOn(openApiGenerate) }
}

Last but not least, let's make sure the source set includes our generated code:

sourceSets[SourceSet.MAIN_SOURCE_SET_NAME].java {
    srcDir(generatedCodeDirectoryPath + File.separator +
         "src" + File.separator + "main" + File.separator + "kotlin")
}

If you'll run the build command now in Gradle you should see something similar in your project files:

build directory generated code

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:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-validation")
}

Step 3 - Implement the API! ๐ŸŽ‰

IMG_20190304_180316__01_compressed.jpeg

This stage is the easiest step so far. We will implement our generated interface GreetingApi in our controller. Let's write the code!

@RestController
class GreetingApiController : GreetingApi {

    override fun greetName(
        @RequestParam(value = "name", required = false) name: String?
    ): ResponseEntity<GreetResponse> =
        if (name.isNullOrBlank())
            ResponseEntity.ok(GreetResponse("Hello, world!"))
        else
            ResponseEntity.ok(GreetResponse("Hello, $name!"))
}

We can now run our server and try to call it from the browser:

Greeting from our server

Step 4 - Writing tests

Let's add a few tests to our API! Since we have no actual business logic here, I would have only an integration test for our new code. It will check the flow completely.

Let's start by defining our WebConfig class in our src/main/kotlin directory. We will do so by adding the following content:

@Configuration
@ComponentScan(
    basePackageClasses = [GreetingApplication::class],
    includeFilters = [ComponentScan.Filter(RestController::class)]
)
class WebConfig

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:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [WebConfig::class])
@AutoConfigureMockMvc
@WebAppConfiguration
class GreetingApiControllerTest(context: WebApplicationContext) {
    private val mockMvc = MockMvcBuilders
        .webAppContextSetup(context).build()

    data class TestCase(val name: String?, val expectedResult: String)

    private fun getTestCase(): List<Arguments> =
        arrayOf(
            TestCase(name = "test", expectedResult = "Hello, test!"),
            TestCase(name = null, expectedResult = "Hello, world!"),
            TestCase(name = "๐Ÿ’ฉ", expectedResult = "Hello, ๐Ÿ’ฉ!"),
        ).map { Arguments.of(it) }

    @ParameterizedTest
    @MethodSource("getTestCase")
    fun `should return correct greeting`(testCase: TestCase) {
        // Given uri and a request
        val uri = if (testCase.name.isNullOrBlank()) "/v1/greet"
        else "/v1/greet?name=${testCase.name}"

        val request = MockMvcRequestBuilders.get(uri)
            .accept(MediaType.APPLICATION_JSON)

        // When we call the api
        val response =
            mockMvc
                .perform(request)
                .andExpect(MockMvcResultMatchers.status().isOk)
                .andReturn()
                .response
                .contentAsString

        // Then we expect to have a response with the correct greeting
        val actualGreeting = ObjectMapper()
            .readTree(response)["greet"]
            .asText()
        assertEquals(testCase.expectedResult, actualGreeting)
    }
}

If you did everything correctly, by running the test you should see that you have 2 green tests! ๐ŸŽ‰

Unit test passing

The finished project is also available on my GitHub repository so you can see the finished code there. you can visit by clicking here.

Conclusion

Using OpenAPI allows us to ensure that our integration is smooth, robust, and fast. Whenever you define a new API you should always prefer using some API schema over hand-writing it.

Did you find this article valuable?

Support Yonatan Karp-Rudin by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors
ย 
Share this