The developer responsible for an API needs to ensure that other developers can quickly see when an API version has changed, how it has changed and what this means to them. Without this, the interface between the requesting and responding services becomes chaotic, and unexpected failures in data flows will occur. To avoid this, development teams need a clearly defined API versioning strategy.
This process is relatively easy when dealing with new APIs and applications that require minimal amounts of changes. Even moving from a 1.0 version to a 1.1 should not create major issues. However, problems arise when major version changes take place.
Let's look at some common API versioning scenarios where a strategy is required and explore how an abstraction layer can help.
When will version changes happen?
Ideally, all services should have an API that operates via function libraries that are decoupled from the core code. Developers can change these libraries when needed, and multiple libraries can operate to provide support for multiple versions, as required. However, because developers can make these changes to the underlying libraries, they should use a clearly defined API versioning process.
Let's assume a simple scenario: A calling service (service A) wants to interact with a responding service (service B). Service A has been around a long time, built on simple object access protocol (SOAP). This posed no problem as service B's APIs were also built to handle SOAP-compliant APIs. If everything is left as it is, this relationship between the two services works.
However, service A's team imbued their API with new features that stretch beyond SOAP's built-in capabilities -- such as an extension that enables ASP.NET to customize SOAP requests and responses. If service B is not prepared to support this extension, it finds that calls from service A do not behave as expected and cause significant errors. To fix the problem, the developers on service B need to locate the change and adapt their calling mechanism to the new API.
This problem gets worse as API protocols change. Perhaps the development team working on service B decides to move to stateless services and starts to run on RESTful APIs. Service A is still based in SOAP APIs. Its team needs to version these APIs in a way that allows calls to flow between the two services. Again, there needs to be human communication about when and how service A can adapt so that service communication isn't disrupted.
Support API versioning through URI
There are two distinct ways developers should address this problem when services are out of sync. First, once they rewrite the service and the API, they need to broadcast to all development teams that the old system no longer works, and the consumer services will need to adapt. Then, to actually facilitate the change, they can create an abstraction layer that manages whatever the user sends in and matches it through to what the service is now capable of doing. An abstraction layer frees developers to update an API of their service without blocking older API calls. New users access all the new functionality no matter the API.
The Uniform Resource Identifier (URI) redirection technique uses content within the URI itself to redirect the call to underlying functions. There are three main ways to support API versioning via a URI:
Direct URI versioning. This method uses a direct call to http://apicall.service.com/v1. Developers can add new versions. When they do, old-version users still come in through this /v1 URI, and new users come in through http://apicall.service.com/v2
Custom request header. Here, the URI remains the same across versions, but the API has a header associated with each version:
http://apicall.service.com api-version: 1
http://apicall.service.com api-version: 2
Content-type accept header. The URI stays the same, and a more complex header is associated with each version. A content-type accept header example is below:
http://apicall.service.com Accept: application/vnd.service.v1+json
API versioning through URI still takes a lot of work. Take a moment to picture what that abstraction layer might look like after a few years. It must support all the original calls, but simultaneously handle new calls as developers make changes throughout the life of the responding service. That's a lot to ask the abstraction layer to do, and developers could take a hit on performance for it.
Maintaining two libraries called via direct URIs is not a problem, but imagine that you are on version 10 of an API. That means 10 sets of different direct URIs to support. There's a fair amount of overhead to monitor all those different calls and provide resources for 10 sets of libraries running at once. Deprecating old versions makes sense, in concert with URI-based versioning. Commonly, development organizations maintain the last two API versions alongside the current one.
Developers are highly likely to make a high rate of changes to an API in its early stages, as both the team and the API users find problems and request additional functionalities. Therefore, expect to find that 0.x versions are more changeable, with lots of functionality just appearing or disappearing as the API develops. It's common to see developers and independent software vendors support multiple versions of their API as they see fit in order to deal with backward compatibility -- usually three.
But as the API matures, adhere to a common nomenclature. Use 1.x, 1.y naming, or even 1.x.y and 1.x.z, for minor changes that minimally affect API calls. Reserve 1.x, 2.x, and so on for changes that impact the functioning of existing API calls. So, for a nonbreaking change that adds a new function, those who are utilizing your API via a direct URI call will use a call such as:
The same libraries as the v1 API work, with the new functionality embedded within the library as required. All existing calls still operate as they did before the change was introduced, but new calls can be added into the requesting service easily and without changing any other part of the request.
Where a breaking change is introduced, use a different version number, such as:
Requests using the old API calls will fail if placed against the new libraries that are required to support the change. This means that all v1 calls must go via the v1 URI and all v2 calls must go via the v2 URI.
Although the above examples cover versioning within direct URI calls, this numbering system works just as well for custom request and content-type accept headers. The changes are the version numbers and how your abstraction layer points the call to the right libraries.