Rest API Documentation and Client Generation with OpenAPI

Microservices architecture helps in building an application as a suite of multiple fine-grained smaller services. Each of these services run in its own process and are independently deployable. They may have been developed in different programming languages and may use different data storage techniques and communicate with each other via lightweight mechanisms like RESTful APIs, message queues etc. Now that services(API clients) need to know how to interact with each other, there is a need of API documentation that should be clear, informative and easy to go through.

OpenAPI makes documenting these RESTful services very easy and can be done by adding few annotations to a Spring based project. Besides rendering documentation, it allows the API consumers to interact with the APIs without having their own implementation logic in place. Also, it can be used to generate the API client libraries for over 50+ languages. In this post, I’ll cover how to use OpenAPI to generate REST API documentation and Java clients for a Spring Boot project.

Our sample application implements a set of REST endpoints to expose the employees and departments resources of an organization. Following is the UML model of the sample application we will look at:

Following controller exposes the Rest APIs:

package com.xyz.openapi.server.controller;

import com.xyz.openapi.server.model.Department;
import com.xyz.openapi.server.model.Employee;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api")
public class OrganizationController {
    private List<Employee> employees = new ArrayList<>();

    @GetMapping(value = "/employees")
    public EmployeeList getAllEmployees(@RequestParam(required = false) String deptId) {
        List<Employee> employees = this.employees.stream()
                .filter(emp -> deptId == null ||
                        (deptId != null && emp.getDepartment() != null && emp.getDepartment().getId().equals(deptId)))
                .collect(Collectors.toList());
        return EmployeeList.of(employees);
    }

    @GetMapping(value = "/employees/{id}")
    public Employee getEmployee(@PathVariable String id) {
        Optional<Employee> optional = employees.stream()
                .filter(emp -> emp.getId().equals(id))
                .findAny();
        if(optional.isEmpty()) {
            throw new IllegalArgumentException("Employee does not exist for id: "+id);
        }
        return optional.get();
    }

    @PostMapping(value = "/employees")
    public String createEmployee(@RequestBody Employee emp){
        emp.setId(UUID.randomUUID().toString());
        employees.add(emp);
        return emp.getId();
    }

    @PutMapping(value = "/employees")
    public String updateEmployee(Employee updatedEmp){
        employees.stream()
                .filter(e -> updatedEmp.getId().equals(e.getId()))
                .findFirst()
                .map(emp -> {
                    BeanUtils.copyProperties(updatedEmp, emp);
                    return emp;
                })
                .orElseThrow();
        return updatedEmp.getId();
    }

    // Few other APIs for Department resource follows here
}

To add OpenAPI to our project, the following dependency has to be added to gradle:

// OpenAPI Documentation
dependency("org.springdoc:springdoc-openapi-ui:1.5.8")

Now add the OpenAPI configuration to the project:

@Configuration
public class SwaggerConfiguration {
    @Bean
    public OpenAPI openAPI() {
        Contact contact = new Contact();
        contact.setEmail("help@xyz.com");
        contact.setName("XYZ Support");
        contact.setUrl("http://www.xyz.com");
        return new OpenAPI()
                .info(new Info().title("Employee APIs").description("Description here..")
                        .version("1.0").contact(contact)
                        .license(new License().name("2015-2021 XYZ LLC All Rights Reserved")));
    }
}

Now when we run the application, the API documentation can be accessed at http://localhost:8080/swagger-ui/. Here is a snapshot of how it looks like:

The schemas section of the documentation lists all the model classes(and their structure) involved in the API transactions. Following is the schema for the sample application:

We can annotate the APIs with Schema definitions to have more control over the API documentation generation process. In the below code snippet, we have made use of the following annotations:
1. Operation: Define additional properties for the API Operation.
2. ApiResponses: Container for repeatable ApiResponse annotation
3. ApiResponse: The annotation may be used at method level or as field of Operation to define one or more responses of the Operation.
4. Schema: The annotation may be used to define a Schema for a set of elements of the OpenAPI spec, and/or to define additional properties for the schema. It is applicable e.g. to parameters, schema classes (aka “models”), properties of such models, request and response content, header.

@RestController
@RequestMapping("/api")
@Tag(name = "Organization Controls", description = "API Endpoints to operate on Employee and Departments")
public class OrganizationController {
    private List<Employee> employees = new ArrayList<>();
    private List<Department> departments = new ArrayList<>();

    @GetMapping(value = "/employees")
    @Operation(summary = "If departmentId is not passed, get all employees. Otherwise get employees from specific department.")
    @ApiResponses({
            @ApiResponse(responseCode = "200", content = {
                    @Content(schema = @Schema(implementation = EmployeeList.class))
            }),
            @ApiResponse(responseCode = "500", content = {
                    @Content(schema = @Schema(implementation = ErrorResponse.class))
            })
    })
    public EmployeeList getAllEmployees(@RequestParam(required = false) String deptId) {
        List<Employee> employees = this.employees.stream()
                .filter(emp -> deptId == null ||
                        (deptId != null && emp.getDepartment() != null && emp.getDepartment().getId().equals(deptId)))
                .collect(Collectors.toList());
        return EmployeeList.of(employees);
    }

    // More methods here
}

After adding the above annotations, here is how the API documentations looks like:

One more benefit of OpenAPI documentation is that the APIs can be executed on UI directly

Now that the documentation is ready, the next step is to generate Java API clients. We will use OpenAPI generator gradle plugin for this purpose. The other services can simply use the API client once generated, instead of writing their own client, to interact with API. The generated client encapsulates the logic of making REST calls to the exposed APIs behind the scenes. In order to do this, we need to add the following plugin to gradle:

// https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator-gradle-plugin/README.adoc
plugins {
    id "org.openapi.generator" version "5.1.0"
}

openApiGenerate {
	// The input specification to validate. Can be accessed at http://localhost:8080/v3/api-docs
    inputSpec.set("$rootDir/api-docs.json")
	// The output target directory into which code will be generated
    outputDir.set("$rootDir/thinclient")

    groupId.set("com.xyz.openapi")
    id.set("openapi-thinclient")
    version.set("0.0.1-SNAPSHOT")

	// Package for generated API classes
    apiPackage.set("com.xyz.openapi.thinclient.api")
	// Root package for generated code
    invokerPackage.set("com.xyz.openapi.thinclient.invoker")
	// All the model classes being used in API will be in this package
    modelPackage.set("com.xyz.openapi.thinclient.model")

	// Language in which client has to be generated. Please refer: https://openapi-generator.tech/docs/generators/java/
    generatorName.set("java");
	// REST API library to use. For Ex: jersey1, jersey2, okhttp-gson etc
    library.set("resttemplate")
	// A map of options specific to a generator. To see the full list of generator-specified parameters, please refer to [generators docs](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators.md).
    configOptions.put("serializationLibrary", "jackson")
}

Now we can run gradle openApiGenerate to generate the classes in the path configured above in gradle configuration. Here is the structure of generated API client generated:

We can now publish the generated client to repository so that it can be used like any other dependency in the other micro-services. Also, gradle openApiGenerators lists all the languages in which the client can be generated. So, we can generate the client in the language of our choice from the specified list.

You can find the source code for this post on GitHub

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s