SaFi Bank Space : REST API versioning and client lib generation

Mandatory way of versioning REST API:

Generating OpenApi documentation must be part of every service’s build.

To ensure consistent versioning, the following must be done in ever service:

  1. openapi.properties must define the version of the API with an integer. It must contain a line similar to micronaut.openapi.expand.api.version=1

  2. openapi.properties must have the target file name the same as the service name. For example: micronaut.openapi.target.file=build/tmp/kapt3/classes/main/META-INF/swagger/output-manager.yml

  3. Application.kt must refer to the api version in the @OpenAPIDefinition using the following string as version: version = "\${api.version}" (note the \ , it is important to be there!)

There is a check in every pull request that requires the increment of the API version if there is any change in the generated OpenApi service-name.yml file.

Optional steps to generate client libraries:

To avoid boilerplate code for DTO models and http-clients, repeated throughout different apps consuming REST APIs of other micro-services, we can generate API client libraries.

The client library should abstract away all the details of making a REST call. Works as a plug-and-play library that is simply added as a dependency, injected as a bean and ready to use.

Implemented using Open API specification, client code is generated based on the same generated yaml spec, used by swagger. Client lib is a micronaut HTTP client, so it can be easily injected.

To enable generation of client libraries the following has to be done:

  1. build.gradle.kts must define the group of the project like this: group = project.properties["group"]!!

  2. gradle.properties must contain the group definition, for example group=ph.safibank.exampleservice

  3. add id("ph.safibank.common.generated.client.publisher") version "1" to the plugin list. Make sure that our plugin repository is defined in the settings.gradle.kts

    1. pluginManagement {
          repositories {
              gradlePluginPortal()
              maven {
                  url = uri("https://asia-southeast1-maven.pkg.dev/safi-repos/safi-maven")
              }
          }
      }
  4. Add the client generation step to the service’s release and deployment Github workflow. Here is an example: https://github.com/SafiBank/SaFiMono/blob/main/.github/workflows/app-output-manager.yml#L20-L24

    1. Generate-API-client:
          uses: ./.github/workflows/_app-generate-api-client.yml
          with:
            microservice_name: output-manager
          secrets: inherit

Details and explanation

Lifecycle:

The mandatory first three steps are there to have a single source of truth in the service for the REST version it provides.

The optional steps and the added plugin does the following:

  1. generates client source code from openapi spec

    • each module exposing REST API should already have configuration and setup for swagger

    • during the build oas yaml spec is generated in: services/{module}-manager/build/tmp/kapt3/classes/main/META-INF/swagger/{module}-manager.yml

    • the yaml spec is used to generate client sources

  2. build and package generated project into jar

    • gradle builds sources generated in $buildDir/generated/openapi directory

    • this creates a jar file with the same version as the surrounding project

  3. publish jar into safi maven repo

How to


1. Use in dependant app:

  • add dependency in gradle.build.kts, e.g.:

dependencies {
    implementation("ph.safibank.accountmanager:template-service-api-client:1")
}

  • set base path property in application.yaml, e.g.:

    • key for each api-client can be found in generated DefaultApiinterface)

account-manager-api-client-base-path: ${SAFI_ACCOUNT_MANAGER_URL}

  • import, inject and use in code with optionally overwriting it’s default name:

import ph.safibank.accountmanager.client.api.DefaultApi as AccountManagerClient
// other imports

open class TransactionService(
    @Inject val accountManagerRestClient: AccountManagerClient
) {
// bunch of other code. 

  accountManagerRestClient.getAccount(id, true)

// bunch of other code


FAQ:

  • Q: Need to have more insight into the communication?

  • A: The debugging is a bit problematic, but to have more control and insight, increase logging for http-client. Trace level gives you even the payloads.

    • <logger name="io.micronaut.http.client" level="TRACE" />

  • Q: Not sure if your client was generated or not available when used as dependency?

  • A: Several things can be verified about the build generating a client lib and publishing

  • Q: Do I need to keep the clients always at the last version?

  • A: No, our REST API versioning approach is trying to enforce backward compatibility. Using older versions should be OK. You’ll only need to upgrade if you need functionality provided only by a newer version or if there is an announcement about a breaking change. Be mindful about other teams and try to use the latest available version anyways.

Attachments: