At Linn we develop and maintain a variety of HTTP APIs to support the operations of the company. Over time the number of dependencies between these APIs has increased, and it became apparent that we needed a reliable way to introduce changes without breaking existing clients. In this post I describe how we used media type parameters to version our APIs.
What’s The Problem?
Let’s say we have an API which returns user resources like this:
But we want to enhance our API to support cases where we don’t know the user’s first and last name, but we do know their full name. We might want to change our API to respond like this:
Depending on the information available.
This represents a breaking change since clients which were written to expect the
lastName properties would break if we made this change. I’ll discuss what represents a breaking change a bit more later.
To avoid the need to modify every client when we make a change like this, we need to introduce a new version of our API, and provide a means for clients to indicate which version they want.
How Do Other People Solve This?
- Specify the version in the URI (/v1/users/42)
- Specify the version using a query parameter (/users/42?version=1)
However, both of these violate the rule that cool URIs don’t change.
Another option discussed by Michael is to specify the version as a parameter of the media type in the request. This is the option that we decided to implement for Linn since it avoids changing URIs and it makes use of an existing HTTP header, rather than creating a custom one.
What’s a Media Type?
A Media Type describes the content of an HTTP request or response such that the recipient (e.g. a browser) knows how to handle it. For example, the page you are reading now was sent from the server with a media type of
text/html, which your browser knows how to display.
When a client issues an HTTP request, it can indicate what media types it would prefer to receive by using the Accept HTTP header. This post by Sebastien Lambla goes into detail on the structure of media types, how they should be used, and how you should go about defining your own.
How Does Your Solution Work?
Before introducing version support for our APIs we typically used the generic media type
application/json for all our resources.
In order to support versioning we introduced vendor specific media types, e.g.
application/vnd.linn.user+json. This allowed us to define a version parameter for the media type, e.g.
So a request like this:
Might receive a response like this:
But a similar request specifying a different version parameter:
Would receive a response like this:
If a client does not specify a version parameter then the most recent version is returned. If a client requests a version which is no longer supported - a 406 (Not Acceptable) response is given.
Since the Accept header can contain multiple media types, clients can request
application/vnd.linn.user+json; version=1,application/vnd.linn.user+json which can be interpreted as ‘give me version 1 if you support it, otherwise - give me the latest version you do support’.
When Do You Introduce a New Version?
If we introduce a breaking change to a resource - then we introduce a new version, e.g.
application/vnd.linn.user+json; version=3. Clients that understand the new version can request it, but older clients can continue to request earlier versions.
Since we are generally in control of all of our clients, we have the liberty of upgrading them to support the latest resource version as time permits. Once all clients have been updated - we can remove support for the older versions to reduce our maintenance effort.
What Constitutes a Breaking Change?
Some changes to a resource are non-breaking and therefore do not require a version increment.
Adding a new property to a resource should not break existing clients. New clients should be able to take advantage of the additional property by requesting the existing version.
Renaming a property is a breaking change (including changing the case). So for example, if you wanted to change a resource which looks like this:
Then you would need to introduce a new version.
Removing a property is a special case, because you may be unable to continue to support the property on the original version of the resource if you are no longer able to populate it with a sensible value. If you can still populate it for the existing version, then you should introduce a new version with the property removed. If you cannot return it, then you have no choice but to remove the property from the existing version and ensure all existing clients are modified as required.