J

Using the 'Accept' header for HTTP API versioning

14 Jul 2014

Something that all API’s need to consider (no matter how simple they are!) is how they will handle adding and upgrading functionality once someone gets a hold of it.

Sure, you could just wing it and rely on your test suite to give you some confidence in just rolling it out…but what about those weird and wonderful ways others may be leveraging your API? How will they cope with your update?

One approach that is often taken is to include the version number within your endpoint routes - an example would be https://api.example.com/v1/.... While this definitely works, it does create some issues with cache negotiation and clients holding onto outdated links.

Cache negotiation pitfalls

A large portion of caching identifiers are based on URI unless configured otherwise which means that https://api.example.com/v1/users will have a different cache key to https://api.example.com/v2/users. While this is desired behaviour you are potentially paying for the same cache data to be served even though it could be identical and when you pay according to the traffic offset by this service, it can quickly grow out of control. Should you be lucky enough to have a self hosted service you have the additional overhead of the same cached data being stored X times where X is the number of API iterations are accessible.

People bookmarking and holding onto outdated links is a problem that is as old as the internet itself and it probably won’t be going away any time soon. So help yourself out by not creating more issues when https://api.example.com/v1/... no longer works because version 1 of your API has been deprecated and the newly released version is now located at https://api.example.com/v2/... instead.

Now that you know the issues, what is the solution? You guessed it, the HTTP ‘Accept’ header.

The Breakdown™

Using the HTTP Accept header is relatively straight forward and looks a little something like this:

Accept: application/vnd.{app_name}.{version}+{response_type}
  • vnd (as per RFC 4288) is the method of registering as a “vendor” and that you plan to interchange using this context.
  • app_name is a field that identifies your application or product.
  • version will contain the version number you wish to use to distinguish between iterations or releases. Common values are ‘v1’, ‘verison2’ or even ‘beta’.
  • response_type covers what structure you plan to send the response as. E.g. ‘json’, ‘xml’, etc.

Using the HTTP Accept header

Now that you have a fair idea on why it benefits you to use the HTTP Accept header and what it looks like in the wild, it’s time to write some code to use it.

In this example an endpoint to return a user’s record has been setup. Unknown to the general public, a new field has been added to the API however while we are still within the testing phase we only want users who are a part of the beta program to see it.

get '/user/:id' do
  # .. Some magic to get the user based on ID. I will just assume this is done
  # and you have a User object under a local variable called 'user'.

  response_data = {
    name: user.name,
    email: user.email
  }

  if request.headers["Accept"].include? "beta"
    response_data[:plan] = user.subscription_type
  end

  response_data.to_json
end

To see the results all we need to do now is send a request to the endpoint specifying the correct HTTP ‘Accept’ header.

$ curl https://api.example.com/user/1

{
  "name":"Jacob",
  "email":"[email protected]"
}

Now, let’s specify the ‘beta’ HTTP Accept header so that we can take advantage of that new field.

$ curl -H "Accept: application/vnd.example_app.beta+json" \
  https://api.example.com/user/1

{
  "name":"Jacob",
  "email":"[email protected]",
  "plan":"medium"
}