We at SCC Team (and at SUSE in general) are providing more and more APIs with the wonderful HTTP REST approach. APIs evolve over time, often unexpectedly — so it makes sense to get into some API versioning best practices right from the day 0. I was asked to join Crowbar guys' discussion to share my SCC experience with versioning APIs. This article is an attempt to formalize our solution and prepare it for a wider audience.
So, imagine you have different API consumers out of your area of control. Some of them definitely will lag behind the latest release. As a backend server developer, you need to find a way to serve them all with the appropriate API versions.
You'll have to invent some smart custom solution so you know which API to serve which client, right?
Actually, no. HTTP already has all you need, just use it! The technique is called Content type negotiation, and exists as IETF RFC for a looong time.
But how does one use those text/html
and application/json
to serve the cause?
Well, there's an HTTP RFC 6838 for custom vendor-specific MIME types.
It reserves a whole "vendor tree", with free-form entries looking like type / vnd. producer's name followed by media type name [+suffix]
.
Crowbar's protocol uses application/json
as an underlying data format, so the resulting MIME type will be application/vnd.crowbar+json
.
Or, when we add a version number to that type (it was the original intention, wasn't it?), application/vnd.crowbar.v2+json
.
RFC states that "anyone who needs to interchange files associated with some product" can register such a new vnd
entry.
Usage of unregistered vnd
types is not frowned upon in the community, so no need to hurry with your registration either.
Settle the API, make sure that it works, release it to the public, and only then fill in the form.
So, we ask for application/vnd.crowbar.v2+json
from the second generation of Crowbar clients and that's it, correct?
From our experience with SCC-related tools, APIs grow frequently and experience breaking changes infrequently. So we want to add some backwards-compatibility indication to our API versions, as semantic versioning did with software release versions.
Our custom MIME types now look like application/vnd.crowbar.v2.3+json
, and exposes a couple of important properties:
A single server handler (running the latest version) can handle all previous minor versions.
Your 1.7
server provides every endpoint your 1.3
client ever wants to see.
Client's requests provide enough data and context for server's handlers.
And server's responses can have some extra JSON fields — ignore those.
Everything is backwards-compatible, yay!
Every major change requires a separate server handler. Your endpoint now requires some additional data from client — increase the major version. You've removed some URLs from API — increase the major version. You've finally implemented HTTP status codes for errors, instead of response headers — you know what to do. (Why didn't you do that from the beginning, by the way?!)
Sometimes it might even be a good idea to branch your codebase for a new major API version.
Then you can run two separate server instances with two different API versions.
Your can route client requests to the right backend server basing on the Accept
header contents.
There's a basic example of such routing for nginx at the bottom of this article.
Imagine you have some not very up-to-date server, which supports API versions 1.3
and 2.1
:
1.3
is in "maintenance mode" and hasn't been updated for a long time.
2.x
branch evolves rapidly, so your shiny brand-new client requires at least 2.4
to get data right.
With properly implemented content type negotiation on both sides, your client will send Accept: application/vnd.foobar.v2.4+json, application/vnd.foobar.v1.3+json; q=0.1
to the server.
Server will be able to serve only the less prioritized API, so the answer will be with Content-Type: application/vnd.crowbar.v1.3+json
.
And that's it your API versioning scheme just provided your some (weak) forward-compatibility guarantees in addition to (strong) backward-compatibility ones.
Sometimes your clients don't care about API versions. Maybe they are writing curl commands in their CLI. Maybe they're toying with API in some fancy GUI. Maybe it's some unimportant code, and breaking the integration won't cause any tears.
This is not a problem.
Just route to the most recent version of API, process the request and state your current API version in the ContentType
header.
The main trick here is to specify your version even in case of request failures. Then your client can spot the version change in the reply of the API call which "was working OK just before now. Well, maybe a week ago. Or two..." Also this will make API version probing trivial: just GET any URL and look at the content-type in the response.
- be a good webizen, use HTTP built-in content type negotiation for your API versioning;
- create a custom MIME type from vnd tree;
- bump version in your serverside code on every tiniest change;
- indicate version in your responses, especially in error cases;
- being able to handle (and negotiate!) different major versions in parallel is not always easy, but sometimes quite helpful.
You might want to keep your incompatible major versions in different application instances. Then you can route your incoming requests to the right backend (with nginx):
map $http_accept $api_backend {
default "localhost:3002";
"~application/vnd\.crowbar\.v2\+json" "localhost:3002";
"~application/vnd\.crowbar\.v1\+json" "localhost:3001";
}
proxy_pass http://$api_backend/$uri$is_args$args;