February 25, 2022

Evolving an API while keeping backwards compatibility

The Ntropy API is an HTTP API that is made of a few endpoints backed by powerful Machine Learning models.

Being a startup and thus having new companies join our customer base (bringing new needs and usage patterns) means that we frequently have to change and adapt the Ntropy API. It is often said that anything you release publicly needs to be maintained forever, so great flexibility to our customer needs comes with great responsibility to maintaining backwards compatibility.

Maintaining backwards compatibility is only one side of the coin. The other issue that comes with it is to make sure that the API is simple to understand and use and that new customers use only the latest version of the API. This is where the core topic of this blog post lies, so let’s first discuss how we describe the provided API and communicate to our customers.

OpenAPI and SwaggerUI

There are many different ways to describe an HTTP API. Fortunately a number of companies of the tech industry have gathered together to come up with a common standard format of HTTP API representation called OpenAPI (originated as Swagger at SmartBear Software).

OpenAPI enables various possibilities and tools like client code auto-generation for different programming languages. One of those tools that we use here at Ntropy is the SwaggerUI which allows us to present the API in a nice, commonly familiar UI which even enables our existing or potential customers to interactively try it out right from the webpage.

Ntropy API in SwaggerUI

Connexion and API-First Design

In addition to allowing to specify the structure of the API’s input and output, OpenAPI also provides means to define some basic validation rules for the actual values as well using JSONSchema. As the popularity of OpenAPI grew, more and more frameworks and libraries came out built around it, one of which is Connexion by Zalando following the API-first design principle used by Ntropy.

Using Connexion we first define the API using OpenAPI and then only implement the handlers for the API endpoints. With this setup Connexion does the heavy-lifting of type and value validation of the input and output as defined in OpenAPI allowing the handler implementations to be focused on the actual work and have cleaner code mostly containing the business logic.

In addition, Connexion includes an optional distribution of SwaggerUI out-of-the box (under the /ui path) that allows us to render the API definition visually as shown in the screenshot in the previous section.

So far so good, but as with everything there are trade-offs, and we need to consider backwards compatibility.

Backwards Compatibility

Imagine that we have some existing version of an API and need to ship new functionality that is not compatible with the current version. Not to disrupt the users of the current version we provide the new functionality as a new endpoint(s) of the API, standard business. Let’s look at the OpenAPI definition and the handlers implementation for the initial version:

Pretty straightforward. We have two endpoints defined in the OpenAPI and appropriate handlers in the Python module using Connexion. Now let’s have a look at the resulting SwaggerUI output:

SwaggerUI for the initial version of the API

So far so good. Now let’s add a new shiny endpoint. As usual, we define it in the OpenAPI definition and then implement the handler.

Everything is good, except that we now have both old and new endpoints in the OpenAPI definition all of which show up in the SwaggerUI:

SwaggerUI including the new endpoint

We would like to have only the new endpoint documented in the Swagger UI while still supporting the old endpoints. One way to do that is to simply remove them from the OpenAPI definition and implement them directly in Flask, but in that case we lose all of the convenience provided by Connexion.

Another option is to use the deprecated flag, the official part of the OpenAPI specification, which, as the name implies, indicates that the endpoint is deprecated. While visually distinctive, the deprecated endpoints still appear in the SwaggerUI like this:

Deprecated endpoints rendered in SwaggerUI

There is no way to hide endpoints which is natively supported by Connexion or SwaggerUI. So we’re on our own with this and need to implement something that will allow us to leave the old endpoints in the OpenAPI while hiding it in the SwaggerUI. To do that let’s first understand how SwaggerUI works.

SwaggerUI and openapi.json

SwaggerUI is a HTML template rendered and served by Connexion, which when loaded in the browser, requests /openapi.json (by default, configurable via SwaggerUI settings) containing the OpenAPI definition, which in turn is generated automatically by Connexion based on the original openapi.yaml that we define as the source of our API definition.

If we could serve our own /openapi.json file but use the rest of Connexion as is, we could change whatever parts of the YAML file we would want. Handily enough, Connexion allows us to disable serving the API definition using the options argument of the add_api function. That’s a good step to start with:

Now if we try to open the SwaggerUI, we will get a 500 error. Looking at the logs we see:

So it looks like when the definition serving is enabled, Connexion is defining a _openapi_json endpoint which outputs the OpenAPI definition in JSON and is used by the SwaggerUI. Let’s try to define it ourselves and then we will have total control over what SwaggerUI shows:

And the SwaggerUI shows…

SwaggerUI using the overridden openapi.json

It works! Now we can manipulate the output as we wish, it is only a matter of inspiration and creativity. The most straightforward way to go from here would be to delete the old endpoints from the openapi[“paths”] and the job is done.

But we’re human beings and tend to forget things. If we eventually remove some hidden endpoint from the original openapi.yaml and forget to remove the manual deletion of it from our custom _openapi_json implementation, that could result in some unpleasant 500 errors.

To prevent such issues let’s stick to having the original openapi.yaml as the source of truth and introduce a new x-hidden boolean flag, set to true for the old endpoints. Our _openapi_json implementation will iterate over all endpoints and delete all those from the output that have x-hidden enabled.

Let’s checkout the SwaggerUI:

SwaggerUI with the old endpoints hidden

Exactly what we needed! As a final step let’s make sure that the old endpoints are still working:

:replace_this_with_any_custom_slack_emoji_you_use_to_represent_joy:

Checkout the diff for the x-hidden implementation here and the full code in the example GitHub repository.

Btw, we’re hiring.

Related posts

May 24, 2022

The False Promise of General Transaction Categorization and the inadequacy of in-house models

Read now
March 3, 2022

War in Ukraine

Read now
April 27, 2022

Using Elyra to create Machine Learning pipelines on Kubeflow

Read now