Recently, I took the task of looking into the topic of API versioning and try to understand why everybody was saying it’s a complicated one, and why there is no general solution to make all parties happy. After spending some time on this subject, I finally got to the conclusion that this is happening most likely because of the disconnection between the SaaS (Software as a Service) providers and their clients, each having slightly divergent interests.
Although this article focuses on medium to large SaaS providers with complex multi-domain APIs, the same principles can be applied even for simple API versioning.
TLDR; The solution is straight-forward: default versionless with optional resources version as query parameters .
I invite you, however, to go through the rest of this article to understand the reasoning behind this solution and get the detailed implementation steps.
I’m also assuming you’re pretty much familiar with APIs, API Gateways and micro-services, so this piece might be addressed mainly to advanced, experienced engineers.
Most SaaS providers, exposes a public facing API gateway for its clients to be able to interact with. For the clients as API consumers, and for SaaS providers as a long-term commitment towards them, there are certain expectations regarding the consistency and the stability of the APIs, following Internet standards and de-facto practices proven over time.
Both SaaS and its clients have a common goal: to use the latest features of the APIs. This is beneficial for clients taking advantage of the latest features and updates and SaaS by minimising cost maintaining historic changes to ensure backward compatibility, but it also means that the APIs will never suffer breaking changes. In practice, this proved to be very hard to impossible over time, thus the need for API versioning was considered as a solution.
The topic of API versioning, along with cache expiration and naming identifiers, is a very difficult one and while certain approaches might solve some of the issues, there is no consensus among API providers for any given solution.
This article aim to propose a API versioning approach which suit most SaaS providers current and long term needs.
The most important rule we need to consider when designing API is that the URI should have less natural constraints and it should be preserved over time. The longer the API’s lifespan, the greater the commitment to the users of the service and its API.
It is hard to anticipate all the resources with their behaviours that would be consumed through the API, but it is enough to correctly define their end-points and the addressing scheme of every resource instance.
Over time one may need to add new resources and new attributes, but the method that API clients follow to access particular resources should not change once a resource addressing scheme becomes public, and therefore final. This method applies to REST APIs by using HTTP verbs (POST, GET, PUT, PATCH and DELETE) and status codes ensuring API clients that used to work without human intervention should continue to work as expected.
The API versioning became a contentious topic especially due to the confusion between the API version as a well-defined set of endpoints referring resources and their behaviours, and API resources versioning.
We can wrongly assume that /v2 in https://dev.example.com/api/v2/users refers to the version 2 of the user resource, and be tempted to split the URI into the base URIhttps://dev.example.com/api and the endpoint /v2/users thus making room for future developments of this resources as /v3/users and /v4/users.
This will disrupt the REST constraints by having a resource URI that would change over time. As a rule of thumb, API resources versions should not be kept in resource URIs, meaning that resource URIs should be permalinks. Sometimes it’s ok to embed the API resource version in the URI for restricted use (like beta testing or debugging) but only for a limited period of time, to avoid a commitment on a transient information.
It is important to understand that the mere presence of the /vX (ex. /v2) in the URI should not denote an API resources version, but rather a provision made for a possible future major refactor of the whole API, while preserving the current one for the committed time frame. Thus /v2 is an integral part of the base URI https://dev.example.com/api/v2/users which does not break the permalink principle as long it does not change over the committed time.
Since embedding the API resource version in the URI is not a wise idea, colliding with the permalink principle, various other solutions are considered, with their respective pros and cons:
Having the API version in the request headers (solution 1 and 2) are less than ideal because of:
It’s worth noting that these are just a few potential reasons, thus discouraging the use of API version in the request headers.
We’re going to investigate why the 3rd solution seems to be the choice for most SaaS APIs. But before that, let’s understand the API Gateway architecture and the different entities subject of versioning.
API providers are exposing a public facing API gateway for its clients to be able to interact with. This API gateway offers a collection of domain-grouped endpoints mapped to corresponding micro-services. Normally, micro-services for each domain are continuously being updated with new features.
There are actually three entities subject to versioning. The first one is the release, which collate all the changes for all domains since the previous one, following the roadmap.
As you already suspected, releases may contains breaking and non-breaking changes, additions, deprecations and removals of various resources part of their micro-services. In the case of semantic versioning of the release, it’s easy to loose track of it and may induce confusion to the clients.
The most common way to version a release is by its date, and it includes a so called release note where all the changes, breaking or non-breaking, are described.
The second entity subject to versioning is the domain. As we saw earlier, the domain is mainly a logical group of several APIs (or endpoints) which can be defined and documented in an OpenAPI (or Swagger) file. Each such file has its own version which should be interpreted as document version not to be confused with API version (although many APIs are defined by just one OpenAPI file).
Diving into the details of the OpenAPI specifications is out of the scope of this article, but if you’re interested, more information about the file structure can be found here.
The OpenAPI document version is also a semantic one and it’s a good practice to follow the rule of bumping majors/minors based on breaking or non-breaking changes. Versioning this entity becomes very important especially in the case SaaS provider or the client decides to start using the OpenAPI files for code generation. Now the release will actually contain updated and old OpenAPI files, each with its own document version.
OpenAPI files have some useful constrains such as uniqueness of endpoints and operationId mainly used for code generation (think of them as the names of the methods wrapping each endpoint call).
Before moving forward, let’s agree what does not qualify as breaking change in the API world:
The third entity subject to versioning is the resource. This is the most important part and actually the changes are always affecting the resources one way or another, breaking or not.
Maybe it’s a good idea now to remind us what is a resource in the API context. Normally, it’s the entity affected by the call of a certain API URI. For instance, if we call GET https://example.com/api/users then we can assume here the resource to be the users.
What happens if the backend engineers decide to introduce a breaking change to the users resource, and also maintain the old way of using while deprecating it?
Due to OpenAPI constrains, we cannot have the same endpoint listed twice in the document, so the engineers will be tempted to embed a resource version in the endpoint (as in /v2/users which means resource user version 2). At a first glance it may look reasonable and the solutions is at least straight forward.
However, now we are facing the undesired situation of multiple identical coexisting resources with different versions. Thus, in the same OpenAPI file we may have /v2/users and /v3/users, both valid endpoints serving more or less the same purpose but with breaking differences of usage between them.
On top of this confusion, each endpoint has an associated operationId field in the OpenAPI file, which mainly will be used to infer the method name and which also must be unique. In the example above, the operationId for /v2/users will be getUsers assuming it's a GET verb, but it cannot be the same for /v3/users, so we may need to change it to getUsersV3 or similar.
This is highly confusing, as one can hardly find the difference between the two endpoints, and now, that the version became evident in method's name, will it be compatible with some other endpoints?
Often, the operationId is going to be changed backward so the /v2/users will be getUsersV2 and the /v3/users will inherit the getUsers one. Without a proper discipline, processes and governance, this will become a very serious issue for those using OpenAPI code generation.
Now we see that embedding resource version into the URI is something we should definitely avoid, because it violates the permalink principle and it induce confusion in OpenAPI files, trying to accommodate the same resource with different versions and different operationId.
The proposed changes will be applied to all the three mentioned levels and will bring consistency and clarity on the use of any APIs. The proposals will also include the details of implementation, the level of approvals and the governance considerations.
The baseURI for the client-facing APIs ideally should be versionless. The proposed URI may look similar to:
https://client.host.domain.com/api
where the client.host.domain.com may contain indications of different hosts for different environments such as production, staging, testing, etc.
The OpenAPI (or swagger) files are normally bound to their corresponding usage domains and contains the endpoints serving only few resources. This will offer enough flexibility in managing the lifecycle of specific endpoints in a consistent way and making use of the included predefined semantic versioning format will serve the purpose continuously tracking the changes.
It is highly recommended to keep the OpenAPI files to a reduced level of complexity, for logical grouping and ease in maintenance. If the same team is developing different API resources, these can be easily referenced in different files based on their logical usage . For example, there is no reason to bundle together the payments and the cards APIs in the same OpenAPI file. These will certainly be developed at different paces and with no dependencies between them.
The OpenAPI files are normally yml (or json) files and they should be treated as code in git repositories with all the implications derived from that, such as changes initiated through branching, CR/PR (change requests), code review and approval and merging in the main branch.
I’ll followup with a separate article on how to properly manage OpenAPI documents , with examples.
This level is the most dynamic and will suffer most changes.
The present proposal consider the resource-level versioning implementation using optional query parameter the most appropriate one. Here’s an example of and endpoint using the optional query parameter version(or ver) to reinforce a given resource version:
https://client.host.domain.com/api/users/regitration?ver=5
This URI clearly state that the API endpoint (which will remain virtually unchanged as baseURI) will be used with the relative /users/registration endpoint (ie. user resource) using its 5th version. Since it's an optional query parameter, its absence will default to the latest resource version. This may come as an issue at first glance as it looks like inducing breaking changes but we'll see below how this might be avoided to the benefit of the clients.
Different versions of of the same resource cannot coexist in a given OpenAPI file for a given document version. The newer version may also deprecate the older one. Indeed, any change in resource version will be normally considered a non-breaking one, thus bumping the minor version of the OpenAPI document version.
There are certainly several advantages which comes from using this approach:
SaaS providers offer multi-tenant APIs, normally based on different API Keys, by using an API Gateway which is capable of parsing and transforming the URIs, headers, request parameters and request payloads, and route the requests to the appropriate microservice. As a consequence, the API Gateway is also capable of temporary adding a request query param with a resource version for a given endpoint, thus preventing any possible breaks at the client side until the breaking change is implemented as per latest resource version.
Since the approach doesn’t limit itself only to client-facing API, it can be easily propagated at the microservice API design level using OpenAPI files. Indeed same rules applies for documents and resources versioning and it will be much easier to cascade the changes to the the API Gateway.
There is also an apparent issue using versionless APIs. If all clients default to the latest resource version and a breaking change cannot be avoided, how can we prevent a crash at the client side?
Given the targeted low occurrence of breaking changes as stated above, these cases will be totally isolated and easy to maintain separately for each client who’s left behind.
For example, a POST call for a select client with a given ApiKey and host like:
https://client.host.domain.com/api/users/regitration
will be easily transformed in the API Gateway, as:
https://client.host.domain.com/api/users/regitration?ver=4
ensuring temporary backward compatibility, until the client aligns with the latest endpoint implementation as per the new OpenAPI document.
Overall, this solution will ensure that no breaking changes will affect the client, while promoting alignment to the latest resource version. It also facilitate the testing of the latest version by explicitly stating it, thus overwriting the default old one in the API Gateway.
API versioning can and should be addressed in a constructive manner, considering all the participating stakeholders: microservice developers, domain specific architects, front-end developers, etc.
Here’s a step by step instructions on how to implement a versionless API versioning system.
If you already have the resource version embedded into the URI, moving completely to the version-less approach is actually the best solution.
It will be rather confusing to add a global API version in the URI, in conflict with the current embedded resource version.
Initially the API Gateway will have a simple task of static routing between the default version or query version (if provided) to the current endpoint until internally updated or sunsetted.
Below we can find a summary of the rules guiding the development of the APIs considering the request query parameter based resource versioning and the OpenAPI (swagger) document versioning.
Originally published in Medium.