Using the Accept Header to version your API


I investigated different ways to version a REST API. Most of the sources I found, pretty much all said the same thing. To version any resource on the internet, you should not change the URL. The web isn't versioned, and changing the URL would tell a client there is more than 1 resource. But actually there aren't multiple resources, it's just a different representation of the same resource. Of course there are cases where you should change the URL; for example if you are changing the API in such a way that its functionality alters. In this particular case you could consider changing the URL as you could reason that it is not the same resource anymore.

But another thing, and probably even more important, you should always try to make sure your changes are backwards compatible. That would mean there is a lot of thinking involved before the actual API is built, but it can also save you from a big, very big headache.

Using the Accept Header

Of course there are always occasions where BC breaks are essential in order to move forward. In this case vesioning becomes important. The method that I found, which appears to be the most logical, is by requesting a specific API version using the Accept header. In this blogpost I will show a possible solution to implementent this with Symfony Routing.

GET /jobs HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.api+json;version=2

The vnd. part is dictated by rfc4288-3.2, and is used to tell standard media types apart from custom ones. A vnd.* media type can be submitted to IANA to be registered as official media type. In theory, you could just use application/json here and add the version parameter, but as the json standard doesn't allow any parameters, it wouldn't be correct to use that.

A great example of a slightly different implementation is the Github API. Github chose to use the latest API version when the client doesn't request a specific version. But there is a good argument to always require the clients to specify a version; when you would default to the latest version, a lot of clients will break as soon as you update your API.

But how?

So now we get to the part how do I get this to work? Let's assume you need to route requests to different controllers based on the given Accept header. To achieve that, you need to parse the Accept header and create routing rules that can use that information.

I created a sandbox with a very simple API that shows how you can route requests based on a requested API version. Version 1 of this API just returns the static list of jobs. In version 2 I ordered those jobs alphabetically based on the job title. To keep things simple, I created a BaseBundle in which custom Router stuff and the JobManager lives.

To be able to switch to the correct Controller, we need to identify the requested version. For this I extended the Router and RequestContext and configured the new classes in parameters.yml.

In the custom Router, I retrieve the Accept header, and set the version in the RequestContext. Thanks to the Symfony AcceptHeader, that's a piece of cake:

public function matchRequest(Request $request)
{
    $acceptHeader = AcceptHeader::fromString($request->headers->get('Accept'))->get($this->acceptHeader);
    // ..
    if (null === ($version = $acceptHeader->getAttribute('version'))) {
        return $this->match($request->getPathInfo());
    }

    $this->getContext()->setApiVersion($version);

    return $this->match($request->getPathInfo());
}

Since Symfony 2.4 you can use the expression language to add conditions for a route.

# Version 1
api1_get_jobs:
    path: "/jobs"
    defaults:
        _controller: "WjzijderveldApiBundle:Api:getJobs"
    condition: "context.getApiVersion() === '1'"

# Version 2
api2_get_jobs:
     path: "/jobs"
     defaults:
         _controller: "WjzijderveldApiBundle:Api2:getJobs"
     condition: "context.getApiVersion() === '2'"

To sum up

Versioning your API's can be pretty hard, especially when you want to do it right! This is just my view on on a part of the problem, what is your view? Discuss it with us on Freenode in #qandidate.

Used resources