How to Automatically Generate Clients for your REST API

In this post we will automatically generate a REST API client based upon an OpenAPI specification . The OpenAPI specification allows REST APIs to be described in a standard programming language-agnostic way, making it possible for both humans and computers to interpret the specification. While working on some code to retrieve additional information ( bunq/sdk_python#148 ) from the bunq Python SDK , we noticed that...

Warpnet icon

In this post we will automatically generate a REST API client based upon an OpenAPI specification . The OpenAPI specification allows REST APIs to be described in a standard programming language-agnostic way, making it possible for both humans and computers to interpret the specification.

While working on some code to retrieve additional information ( bunq/sdk_python#148 ) from the bunq Python SDK , we noticed that this SDK was automatically generated. We ended up monkey patching it, as we couldn’t make a pull request to the SDK and the API specification or SDK generator wasn’t publicly available. This aroused our interest about the automatic generation of API clients.

Write the OpenAPI specification

Even though quite a lot of REST APIs nowadays come included with an OpenAPI specification, not all of them do. In this post we will be using the JSON interface on xkcdas an example of the latter. We will start by describing the OpenAPI specification for two existing endpoints:

GET: http://xkcd.com/info.0.json (current comic)
GET: http://xkcd.com/614/info.0.json (comic #614)

Let’s start by creating a file called openapi.yaml. After filling in some basic information such as the API information and available server(s), we can add the individual endpoints. The specification below contains the endpoint for retrieving an xkcd comic by its identifier.

openapi: 3.0.0

info:
  version: 1.0.0
  title: xkcd
  description: 'A webcomic of romance, sarcasm, math, and language.'

servers:
  - url: https://xkcd.com/
    description: Official xkcd JSON interface

paths:
  # Retrieve a comic by its identifier
  /{id}/info.0.json:
    get:
      tags:
        - comic
      description: Returns comic based on ID
      summary: Find comic by ID
      operationId: getComicById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Successfully returned a comic
          content:
            application/json:
              schema:
                type: object
                properties:
                  link:
                    type: string
                  img:
                    type: string
                  title:
                    type: string

Automatically generate a Python SDK

The OpenAPI Generator allows the generation of API client libraries and documentation for multiple languages and frameworks given an OpenAPI specification. This includes generators for PythonGoJava and many more.

Even though this generator is written in Java, you can use the pre-built Docker image to act as a stand-alone executable. The command below will generate a Python SDK based upon the specification (openapi.yaml):

$ docker run --rm \
  -v ${pwd}:/local openapitools/openapi-generator-cli generate \
  -i /local/openapi.yaml \
  -g python \
  -o /local/out/python \
  --additional-properties=packageName=xkcd_client,projectName=xkcd_client

After execution, the newly generated Python SDK is available in ./out/python and comes included with the necessary documentation to start using it. It even comes with a .openapi-generator-ignore file which lets you specify files to prevent them from being overwritten by the generator.

Improve the OpenAPI specification

Even though we can now use the newly generated SDK, it comes with some very generic class names and ill-favoured function names, e.g.:

import xkcd_client


# Initialize a xkcd API client
configuration = xkcd_client.Configuration(host="https://xkcd.com")
client = xkcd_client.ApiClient(configuration)
api_instance = xkcd_client.api.default_api.DefaultApi(api_client)

# Retrieve a comic by identifier
api_response = api_instance.id_info0_json_get(614)

Let’s improve the OpenAPI specificationto allow the generation of more human friendly code. To rename the DefaultApi class we will need to logically group endpoints by adding the tags option:

...
paths:
  # Retrieve the current comic
  /{id}/info.0.json:
    get:
      # A list of tags to logical group operations by resources and any other
      # qualifier. 
      tags:
        - comic
      description: Returns comic based on ID
...

In order to rename the function DefaultApi.id_info0_json_get we can specify a unique operationId to allow tools and libraries to uniquely identify an operation:

...
paths:
  # Retrieve the current comic
  /{id}/info.0.json:
    get:
      # A list of tags to logical group operations by resources and any other
      # qualifier. 
      tags:
        - comic
      description: Returns comic based on ID
      # Unique identifier for the operation, tools and libraries may use the
      # operationId to uniquely identify an operation.
      operationId: getComic
...

The full OpenAPI specification of the xkcd JSON interface is available on GitHub .

Using the generated Python SDK

After generating the Python SDK we can create a Python virtual environment and install the generated[xkcd Python API client using the following commands:

$ python3 -m venv env
$ source env/bin/activate
$ pip install -e ./out/python

After installing the generated API client, we could, as an example, retrieve the xkcd comic titled “API” using the following Python snippet:

from pprint import pprint

from xkcd_client import Configuration, ApiClient, ApiException
from xkcd_client.api.comic_api import ComicApi


configuration = Configuration(host="https://xkcd.com")

# Enter a context with an instance of the API client
with ApiClient(configuration) as client:
    # Create an instance of the API class
    instance = ComicApi(client)

    try:
        # Retrieve the API comic
        api_response = instance.get_comic_by_id(1481)
        pprint(api_response)
    except ApiException as exc:
        print("Exception when calling ComicApi->get_comic_by_id: {0}\n".format(exc))

Conclusion

In general, it’s quite possible to automatically generate a REST API based upon the OpenAPI specification. Even for REST APIs that do not include an OpenAPI specification by default you can easily describe the API in a specification file to allow the API client generation.

Besides creating multiple API clients in different languages, the biggest benefits are shown when updating the clients as soon as the API specification has been changed. By simply calling the OpenAPI generator again using the updated specifications, you can update all your API clients with little effort. Even though the generated code may seem a bit rough, it’s well documented and could be made more human friendly by improving the specification.

Although this post doesn’t go in depth on authentication, cookie usage or other HTTP methods, they are all usable features of the OpenAPI specification and OpenAPI Generator. Results may differ based upon the language or framework you would like to generate your SDK in, we’re interested to hear about your experience using SDK generators.