Chapter 7

Versioning

Aaaah, versioning. Such a lovely concept, hated by some, loved by others.

For some, versioning is not required as long as you follow the HATEOAS constraint defined in REST and use hypermedia links in your representations. I’m not so sure it’s possible to entirely remove versioning just yet, even with hypermedia. Anyway, we are not going to debate the need for versioning; that’s a topic for the third module. For now, we will assume we need versioning and we are going to study the best ways to handle that in web APIs.

Versioning is considered by many as mandatory in order to create web APIs that can change. Indeed, due to the tight coupling developers usually create between clients and web APIs, versioning an API becomes the only solution to make breaking changes without affecting the clients.

Since we need it, we have to be able to do it correctly. So let’s focus on another very interesting question: what’s the best way to version a web API?

There are a few major styles in play here. A lot of people think versioning should be done in the URL like api1.myapi.com, /v1/users or /users?version=1. Others believe version should be present in a custom header such as X-Version or something like that. Finally, a third group, who usually understand more about REST and HTTP, thinks the version should be in the Accept header as a custom media type like:

application/vnd.myapi-v1+json

Or:

application/vnd.myapi+json; version=1

So who’s right?

Well, no one, really. The solutions range from easy-to-use and widely adopted ones (version in URL) to more complex ones that seem more logical when considering standards and RFCs (version in headers). I can’t tell you which one you need. I can only show you how to implement each one of them and let you decide what you prefer and how you want your clients to access your API.

There will always be someone to tell you that you’re doing it wrong. In the end, it’s your API and if your users are happy, you cannot be that wrong.

Here are all the versioning styles we will cover in this chapter:

Section Versioning Mechanism
Section 1 Subdomain
api1.example.com/users
Section 2 In the URL
example.com/v1/users
Section 3 In the URL with a query parameter
example.com/users?v=1
Section 4 Custom HTTP Header
X-API-Version: 1
Section 5 Accept header with custom media type
Accept: application/vnd.myapi.v2+json
Section 6 Accept header with a version option
Accept:application/vnd.myapi+json;version=2.0

For all these versioning mechanisms, we are going to return the two same representations. One of them will be known as “version 1” (v1) and the other as “version 2” (v2).

The v1 representation will contain the first_name, last_name and age attributes. To justify versioning, the v2 representation includes a breaking change: the removal of the first_name and last_name attributes. In this version, they will be merged together as full_name. It’s a simple and pretty stupid change, but it’s good enough to showcase versioning in the context of an API.

We could obviously just add the full_name as an extra attribute. Unfortunately, in our case, some powerful forces were in play and we had to remove the first_name and last_name attributes from the v2 representation.

Now that we know what we will be doing, let’s go through each versioning style, see how to implement it and learn about its pros and cons. Those different styles are not equal and do not version an API at the same level. That’s why each one of them includes a scope to define exactly how much of the API is being versioned.

In a perfect world, versioning would not be needed.

7.1. Versioning Via Subdomain

Implementation

Sadly, we cannot completely implement it in our application because it requires having a domain name. That doesn’t mean we can’t implement the code, though.

There are basically two choices for this approach: either we create one new application for each version or you we only one application and detect which subdomain the application is accessed from. The second solution is much better since we don’t want to deal with multiple applications when we can just have one.

To implement this type of versioning with a Sinatra API, we need to add an external gem called sinatra-subdomain.

gem install sinatra-subdomain

First, let’s create a new folder in the module_01 folder. Call it chapter_07 and inside it, create a new file named webapi_subdomain.rb.

mkdir chapter_07 && cd chapter_07 && touch webapi_subdomain.rb

Next, we need to configure the subdomain detection coming with sinatra-subdomain. This can be done easily using the following method:

subdomain :api1

Overall, this API is pretty similar to the ones we built before, except for the bits checking which subdomain is being used.

# webapi_subdomain.rb
require 'sinatra'
require 'sinatra/subdomain'
require 'json'

users = {
  thibault: { first_name: 'Thibault', last_name: 'Denizet', age: 25 },
  simon:    { first_name: 'Simon', last_name: 'Random', age: 26 },
  john:     { first_name: 'John', last_name: 'Smith', age: 28 }
}

before do
  content_type 'application/json'
end

# This is the routes for v1
subdomain :api1 do
  get '/users' do
    users.map { |name, data| data }
  end
end

# And this block contains the routes for v2
subdomain :api2 do
  get '/users' do
    users.map do |name, data|
      {
        full_name: "#{data[:first_name]} #{data[:last_name]}",
        age: data[:age]
      }
    end.to_json
  end
end

Now, if we were to deploy this little application and point the two domain names api1.example.com and api2.example.com to it, we would, theoretically, be able to access the two representations of the users resource.

Pros

  • The URNs of our resources are not polluted by a version number; we can call /users and not /v1/users.
  • URIs accessible with GET are easy to share and can be accessed from a web browser.

Cons

  • Although URNs of our resources are not polluted by a version number (/users), their URLs still are (http://api1.example.com). This means that our users concept is accessible with two different unique identifiers that simply return different representations of the same resource. In other words, the same data, but formatted in different ways. This approach breaks the uniform identification properties of URIs.
  • This option requires creating a new subdomain for each version.
  • The application needs to be able to detect and handle different subdomains.

Who Is Doing It This Way?

This method is not very common, and I couldn’t find any famous web APIs that use this kind of versioning mechanism.

My Humble Opinion

I don’t personally like this approach. It becomes quite hard just to add a new version. If you only need to remove or rename a key, you have to create a new subdomain and figure out the routing inside or outside your application. I honestly don’t recommend doing it this way. It shares some caveats with the version in the URL, which we are going to look at right now.

7.2. Versioning In The URL

The first argument in favor of this approach is that you can easily copy a URL and send it to your friends to check out. However this only works for GET requests. For anything else you would need to use curl, Postman or some piece of code to access it. As we’ve seen earlier, the same argument can be made about formats and languages. /users.json and /en/users are a bit easier to share than the equivalent curl requests, at least with non-developers, since any developer should be able to copy/paste a curl request in a terminal.

This approach, like the previous one, is also breaking the uniform identification properties of URIs, since the same resource can have multiple URIs pointing to it.

Implementation

Implementing this solution is easy, especially with Sinatra (but also with Rails and any other web framework).

First, we need to install the sinatra-contrib gem to use namespaces. Namespaces will let us add prefixes in our URLs, such as /v1/users or /v2/users.

gem install sinatra-contrib

Create the file webapi_url.rb in the chapter_07 folder.

touch webapi_url.rb

Now, let’s look at the code. Note how we use the namespace feature to encapsulate our versions within different blocks.

# webapi_url.rb
require 'sinatra'
require 'sinatra/contrib'
require 'json'

users = {
  thibault: { first_name: 'Thibault', last_name: 'Denizet', age: 25 },
  simon:    { first_name: 'Simon', last_name: 'Random', age: 26 },
  john:     { first_name: 'John', last_name: 'Smith', age: 28 }
}

before do
  content_type 'application/json'
end

namespace '/v1' do
  get '/users' do
    users.map { |name, data| data }.to_json
  end
end

namespace '/v2' do
  get '/users' do
    users.map do |name, data|
      {
        full_name: "#{data[:first_name]} #{data[:last_name]}",
        age: data[:age]
      }
    end.to_json
  end
end

Start the server with ruby webapi_url.rb and access /v1/users either with your browser or with curl. The curl commands are available below.

Getting the v1 representation:

curl http://localhost:4567/v1/users

Output

[
  {"first_name":"Thibault", "last_name":"Denizet", "age":25},
  {"first_name":"Simon", "last_name":"Random", "age":26},
  {"first_name":"John", "last_name":"Smith", "age":28}
]

Getting the v2 representation:

curl http://localhost:4567/v2/users

Output

[
  {"full_name":"Thibault Denizet", "age":25},
  {"full_name":"Simon Random", "age":26},
  {"full_name":"John Smith", "age":28}
]

Not too hard to implement, right? Let’s see the pros and cons of this approach.

Pros

  • Resources supporting GET are easy to share and can be accessed from a web browser.
  • It’s harder to miss the version being used. Since the version is more apparent, it can reduce mistakes for inexperienced developers (like using the wrong version or not setting any version at all).
  • This approach is the most popular today and many people consider it a best practice.

Cons

  • Partial versioning is not possible for specific resources. It’s all or nothing, even if that means duplicating the same code or routing v2 endpoints to their v1 counterparts in the shadows.
  • This approach, like the previous one, violates the REST constraints for resources and the uniform identification properties of URIs. Indeed, we have two different resources (/v1/users /v2/users) offering one representation each when we should have one resource (/users) with two different representations.

Who Is Doing It This Way?

A lot of people are versioning their APIs using this approach. Here are a few examples:

Here are a few more that you can look up if you’re interested: Groupon, Youtube, Netflix, Dropbox, Google, Stripe.

My Humble Opinion

While this is the most used approach, it doesn’t necessarily make it the best one. Since it goes against some established principles of the World Wide Web, this shouldn’t be your favorite versioning method (it is not mine anymore). However, it is a very easy way to version your API and a fine solution if you like it, particularly if you control the client side.

Most of the big companies using this approach are kind of stuck with it since they can’t really change it without breaking existing clients.

7.3. Versioning In The URL With a Parameter

Another approach to versioning is using a query parameter in the URI, such as api.example.com/users?version=v1. This option is nice because it doesn’t affect the resource URI (/users). The problem is, what should the server return when the resource is accessed without any version parameter? If the version parameter is always required, it changes the way we can look at the URI for the users resource, which becomes /users?v=1 or /users?v=2; two different identifiers. This solution is acceptable and used by a few APIs, as we will see at the end of this section.

Let’s see how we can implement it.

Implementation

To implement this versioning mechanism, we only need one route /users which will either return the representation of the users list for one of the existing versions, or an error code if no version is specified. Finding the right HTTP code for this situation is tricky. Since we are doing things that should be done in a different way according to the HTTP RFC, we are on our own to pick the best status code.

We could either go with a 300 Multiple Choice or a 404 Not Found. 300 Multiple Choice is used to tell the client that the requested resource has multiple representations available, each one with its own specific location. The server must return the URIs of these locations. We have already studied 404, which would also match the situation if we include the query parameter in the URI. Let’s go with 300 to learn more about it.

Create a new file to hold this new little API.

touch webapi_parameter.rb

First, let’s see how our 300 response-body will look. Let’s put it inside its own method to cleanup the route. As you can see, we simply return a hash (soon to be serialized as JSON) to the client, with a message explaining what’s happening and what are its different options.

def present_300
  {
    message: 'Multiple Versions Available (?version=)',
    links: {
      v1: '/users?version=v1',
      v2: '/users?version=v2'
    }
  }
end

And here is how we will use it in the route. If the client forgets to put the version parameter or tries to use a version number that’s not supported, we will return 300 with the JSON document built above.

unless params['version'] && versions.keys.include?(params['version'])
  halt 300, present_300.to_json
end

This takes us to the /users route, which contains a hash named versions with lambdas that can be passed to the map method to serialize the users the way we want. The method named present_v2 is defined as a helper method in the complete file below.

get '/users' do
  versions = {
    'v1' => lambda { |name, data| data },
    'v2' => lambda { |name, data| present_v2(data) }
  }

  unless params['version'] && versions.keys.include?(params['version'])
    halt 300, present_300.to_json
  end

  users.map(&versions[params['version']]).to_json
end

Put together, it looks like this:

# webapi_parameter.rb
require 'sinatra'
require 'sinatra/contrib'
require 'json'

users = {
  thibault: { first_name: 'Thibault', last_name: 'Denizet', age: 25 },
  simon:    { first_name: 'Simon', last_name: 'Random', age: 26 },
  john:     { first_name: 'John', last_name: 'Smith', age: 28 }
}

before do
  content_type 'application/json'
end

helpers do
  def present_300
    {
      message: 'Multiple Versions Available (?version=)',
      links: {
        v1: '/users?version=v1',
        v2: '/users?version=v2'
      }
    }
  end

  def present_v2(data)
    {
      full_name: "#{data[:first_name]} #{data[:last_name]}",
      age: data[:age]
    }
  end
end

get '/users' do
  versions = {
    'v1' => lambda { |name, data| data },
    'v2' => lambda { |name, data| present_v2(data) }
  }

  unless params['version'] && versions.keys.include?(params['version'])
    halt 300, present_300.to_json
  end

  users.map(&versions[params['version']]).to_json
end

Let’s give it a try. Start the server with ruby webapi_parameter.rb and make the following request with curl (or your browser).

curl http://localhost:4567/users?version=v1

Output

[
  {"first_name":"Thibault", "last_name":"Denizet", "age":25},
  {"first_name":"Simon", "last_name":"Random", "age":26},
  {"first_name":"John", "last_name":"Smith", "age":28}
]

Great, it’s working fine. I’ll let you test version 2 by yourself. Let’s see what happens when we don’t put a version.

curl http://localhost:4567/users

Output

HTTP/1.1 300 Multiple Choices
Content-Type: application/json
[More Headers]

{
  "message":"Multiple Versions Available (?version=)",
  "links":{
    "v1":"/users?version=v1",
    "v2":"/users?version=v2"
  }
}

Awesome, now the client can just query for whichever version it wants.

Pros

  • URIs accessible with GET are easy to share and can be accessed from a web browser.
  • It’s easier to see which version is being used. Since the version is more apparent, it can reduce mistakes for inexperienced developers (like using the wrong API or not setting any version).

Cons

  • Based on the definition of URIs in RFC 3986, query parameters are meant to be used to identify a resource within the scope of a given URI. We are doing it to specify which representation of the resource we want, a feature that (unfortunately) does not follow this RFC.
  • This approach is not frequently used nowadays; the version in the URL we saw before is more widely accepted. I’m not sure what the point is of using this instead of either the most used mechanism (URL) or of something that follows the RFCs definitions (HTTP Header, see below).

Who Is Doing It This Way?

Paypal was using this approach initially, but has since switched to versioning in the URI. However, eBay still seems to be doing it this way for some of their APIs.

My Humble Opinion

As said in the cons section, I don’t see the point of using this approach; especially since there are more widely accepted solutions, not to mention smarter ones.

7.4. Versioning In A Custom Header

We’re beginning to get away from the URI realm; now, it’s time to talk about HTTP header fields. If you want to respect the REST constraints and the HTTP RFC, and build scalable web APIs, one of the following versioning mechanisms will be the right one for you.

There’s a lot of debate about whether to use custom HTTP headers or not. Since you can’t be sure that a web API is behind a proxy or not, and know if the proxy will eventually forward all the headers it received, using custom HTTP headers can be risky.

Let’s see the most common examples of custom headers that are used. Note that originally, custom headers were supposed to start with the X- prefix (for extension), but this was deprecated in the RFC 6648. This leaves us with three header names that can be used to hold the version the client wants to access. They are not official and there are no standards, it’s simply what people have agreed upon over time.

  • X-Version: This header was used before the deprecation of X-. It can still be found in the wild and will contain the version number requested by the client, like X-Version: 1.0.
  • Version: Replacement for X-Version, this header is not defined in the HTTP RFC but was defined in the RFC 4229 as a provisional header. Just like X-Version, it can be used to contain the desired version, such as Version: 1.1.
  • Accept-Version: The set of HTTP headers starting with Accept- are all made for content negotiation, so it sounds like a good idea to use this header to specify which version of the resource is desired.

So which header is better? Well, first of all, it is usually recommended not to add more HTTP headers if it can be avoided. That being said, we will make an exception for this section but we will see later on that there are better alternatives.

What we need to remember is that resources and representations are two different things. You should never have to version a resource (reminder: resources are just ‘concepts’), since only representations tend to change in their formats.

A big problem that comes with using custom HTTP headers to send the version is caching. HTTP comes with a set of features to allow a client to cache responses. That’s awesome, because you really don’t want your web API to go down in flames when you have millions of calls asking for the same thing over and over again. The problem is that many people don’t implement HTTP caching in their API. Lucky you, the next section is focused on caching. We will see how it works and you will understand why custom HTTP headers can prevent us from allowing clients to cache responses.

Note that with HTTP headers, we are considering versioning as another layer of content negotiation. This means that the server needs to handle a lot of situations when incorrect or missing versions are requested.

For now, let’s implement our API using the Version header so we can see how it would actually look.

Implementation

For this implementation we still have our two versions, but we are not going to return 300 to the client. Instead, if the client does not specify a version or sends an incorrect one, we will always return the first version. This is to show you that, as API designers, we are free to follow our own judgement and do what we want. In that case, we are not following any RFCs or standards, so we will need to write a good amount of documentation to let developers understand our own way of doing things.

This is obviously not the best option, right? Following standards tends to save time for everyone. If you agree with that, this versioning system is not going to be for you.

To implement it, we are simply going to check the custom HTTP header sent by the client (Version). Note that, by default, Sinatra will prefix headers with HTTP_ and upcase them, so we need to look for HTTP_VERSION. This actually makes it sound very confusing because this is clearly not the HTTP protocol version.

First, let’s create a new file for this API.

touch webapi_custom_header.rb

Take a look at the code below, especially the /users route. See how we check if the requested version is 2.0 (yes, we are using decimals now)? Anything else will return the version 1.0 of the representation of the users resource.

# webapi_custom_header.rb
require 'sinatra'
require 'json'

users = {
  thibault: { first_name: 'Thibault', last_name: 'Denizet', age: 25 },
  simon:    { first_name: 'Simon', last_name: 'Random', age: 26 },
  john:     { first_name: 'John', last_name: 'Smith', age: 28 }
}

helpers do
  def present_v2(data)
    {
      full_name: "#{data[:first_name]} #{data[:last_name]}",
      age: data[:age]
    }
  end
end

before do
  content_type 'application/json'
end

get '/users' do
  if request.env['HTTP_VERSION'] == '2.0'
    halt 200, users.map { |name, data| present_v2(data) }.to_json
  end
  users.map { |name, data| data }.to_json
end

Let’s make some curl requests to confirm that everything is working. Don’t forget to start the server first.

ruby webapi_custom_header.rb

Asking for version 2.0:

curl http://localhost:4567/users -H 'Version: 2.0'

Output (Getting v2.0 representation as expected):

[
  {"full_name":"Thibault Denizet", "age":25},
  {"full_name":"Simon Random", "age":26},
  {"full_name":"John Smith", "age":28}
]

Asking for version 1.0:

curl http://localhost:4567/users -H 'Version: 1.0'

Output (Getting v1.0 representation as expected):

[
  {"first_name":"Thibault", "last_name":"Denizet", "age":25},
  {"first_name":"Simon", "last_name":"Random", "age":26},
  {"first_name":"John", "last_name":"Smith", "age":28}
]

Not specifying any version:

curl http://localhost:4567/users

Output (Getting v1.0 representation as expected):

[
  {"first_name":"Thibault", "last_name":"Denizet", "age":25},
  {"first_name":"Simon", "last_name":"Random", "age":26},
  {"first_name":"John", "last_name":"Smith", "age":28}
]

We could have also decided that the default would be version 2.0. The problem with this occurs when we deploy 2.0 and make it the default version; clients who are currently not specifying any version will stop working. This should not happen. “Never break the clients” is a fundamental rule you have to follow when building web APIs.

Pros

  • Finally a solution that respects resources and does not break the uniform identification properties of URIs.
  • Header fields are expected to be used to describe the transferred data between server and client, and this approach follows that principle.

Cons

  • Sadly, it limits some powerful HTTP features like caching.
  • There should not be a need to add a new header, since we are only asking for different versions of representations, which should be defined by a media type and the Accept header.
  • This approach does not follow any standard and will require writing more documentation (that you should also maintain).
  • Having a header defining the version makes it harder to share the URI around and prevents testing with browsers.

Who Is Doing It That Way?

  • Azure and a bunch of other Microsoft platforms.

7.5. Versioning With A Custom Media Type

Now we are getting to more interesting versioning systems which kind of make more sense (to me at least). The idea here is not to version in the URI, or in a custom header that would version a resource instead of its representation. No, here we use the media type and the Accept header to do content negotiation. With this approach, the client can ask for a specific version by giving a media-type that can, for example, look like application/vnd.myapi.v1+json.

Remember that the media type, sent in the Accept header, is there to allow the client to ask for its preferred format. Formats are not just JSON or XML; they can be anything. Don’t get too excited though! Just like HTTP headers, new media types are not supposed to be created every morning. They should actually be registered with the IANA. Still, with the vnd prefix (which stands for vendor), using custom media types is accepted by the community.

Implementation

For this implementation, we are going to check whether the client requested one supported media type or not. We will expect one media type in the Accept header to simplify the code, and we will also expect the client to use a media type that we understand. If it does not, we will send back a 406 error with the list of supported media types.

By default, this implementation will always return the version 1 representation of the users resource. This is to avoid breaking any client already using our API when we introduce the second version. Facebook is known to change the default version to the latest version available, which always ends up breaking clients who did not have time to upgrade yet. Not cool.

Before proceeding, let’s create a new file.

touch webapi_media_type.rb

We are going to support 5 media types:

  • */*
  • application/*
  • application/vnd.awesomeapi+json
  • application/vnd.awesomeapi.v1+json
  • application/vnd.awesomeapi.v2+json

To do so, we need to create a hash with the media type as the key and a lambda as the value, representing how the data should be formatted.

v1_lambda = lambda { |name, data| data }
v2_lambda = lambda do |name, data|
  { full_name: "#{data[:first_name]} #{data[:last_name]}", age: data[:age] }
end

supported_media_types = {
  '*/*' => v1_lambda,
  'application/*' => v1_lambda,
  'application/vnd.awesomeapi+json' => v1_lambda,
  'application/vnd.awesomeapi.v1+json' => v1_lambda,
  'application/vnd.awesomeapi.v2+json' => v2_lambda
}

Note that the code could be cleaner if extracted into specialized classes, but for now let’s keep it simple and keep everything in one file.

In the /users route, we will first check for the media type requested in the Accept header. If none is specified, we will default to */*. After that, we will check if the requested media type is supported, simply by checking if the key is found in the supported_media_types hash. If not, we will return 406 with the list of supported media types joined by commas.

Finally, if the requested media type is supported, we will just call the appropriate lambda by passing it to users.map and formatting it with to_json. You can also see that we use a new media type for the data sent back to the client: application/vnd.awesomeapi.error+json. Using such a custom media type allows us to describe its semantics in our documentation and reuse it everywhere in our API while keeping the same format. We are only using it one time here, but you get the idea.

get '/users' do
  accepted = request.accept.first ? request.accept.first.to_s : '*/*'

  unless supported_media_types[accepted]
    content_type 'application/vnd.awesomeapi.error+json'
    msg = {
      supported_media_types: supported_media_types.keys.join(', ')
    }.to_json
    halt 406, msg
  end

  content_type accepted
  users.map(&supported_media_types[accepted]).to_json
end

Here is how it all comes together in a functional and simple web API:

# webapi_media_type.rb
require 'sinatra'
require 'json'

users = {
  thibault: { first_name: 'Thibault', last_name: 'Denizet', age: 25 },
  simon:    { first_name: 'Simon', last_name: 'Random', age: 26 },
  john:     { first_name: 'John', last_name: 'Smith', age: 28 }
}

v1_lambda = lambda { |name, data| data }
v2_lambda = lambda do |name, data|
  { full_name: "#{data[:first_name]} #{data[:last_name]}", age: data[:age] }
end

supported_media_types = {
  '*/*' => v1_lambda,
  'application/*' => v1_lambda,
  'application/vnd.awesomeapi+json' => v1_lambda,
  'application/vnd.awesomeapi.v1+json' => v1_lambda,
  'application/vnd.awesomeapi.v2+json' => v2_lambda
}

get '/users' do
  accepted = request.accept.first ? request.accept.first.to_s : '*/*'

  unless supported_media_types.keys.include?(accepted)
    content_type 'application/vnd.awesomeapi.error+json'
    msg = {
      supported_media_types: supported_media_types.keys.join(', ')
    }.to_json
    halt 406, msg
  end

  content_type accepted
  users.map(&supported_media_types[accepted]).to_json
end

Let’s try what we built. First, start the server.

ruby webapi_media_type.rb

Asking for the default:

curl http://localhost:4567/users

Output (Getting v1 representation as expected):

[
  {"first_name":"Thibault", "last_name":"Denizet", "age":25},
  {"first_name":"Simon", "last_name":"Random", "age":26},
  {"first_name":"John", "last_name":"Smith", "age":28}
]

Asking for version 1:

curl http://localhost:4567/users -H 'Accept: application/vnd.awesomeapi.v1+json'

Output (Getting v1 representation as expected):

[
  {"first_name":"Thibault", "last_name":"Denizet", "age":25},
  {"first_name":"Simon", "last_name":"Random", "age":26},
  {"first_name":"John", "last_name":"Smith", "age":28}
]

Asking for version 2:

curl http://localhost:4567/users -H 'Accept: application/vnd.awesomeapi.v2+json'

Output (Getting v2 representation as expected):

[
  {"full_name":"Thibault Denizet", "age":25},
  {"full_name":"Simon Random", "age":26},
  {"full_name":"John Smith", "age":28}
]

Asking for unknown version:

curl http://localhost:4567/users -H 'Accept: application/vnd.awesomeapi.v3+json'

Output (Getting the list of supported media types):

{
  "supported_media_types": "*/*, application/*, application/vnd.awesomeapi+json,
  application/vnd.awesomeapi.v1+json, application/vnd.awesomeapi.v2+json"
}

Pros

  • This approach is considered by many to be harder to use, test and share. However, it follows the REST and URI principles and allows us to keep our resource identifiers unique, which is quite nice.
  • It uses content negotiation, a standard solution to deal with versioning.
  • It allows us to version only the representations.

Cons

  • Since we are using an HTTP header to ask for a version, it becomes harder to simply share or bookmark a URI.
  • This approach forces us to create custom media types for our API and add new ones for each new version.
  • Parsing the Accept header can be hard. When many media types are included, with different quality factors (\( \sim \) priority), finding the best suited one for our implementation can be a challenge. Some frameworks, like Sinatra, handle it pretty well but many don’t.

My Humble Opinion

I like this approach because it looks clean and follows the principles of the World Wide Web. However, I have a preference for the next approach which I personally find better since there is no need to create new media types for each version.

7.6. Accept Header With A Version Option

We are staying in the media type realm, but this time we don’t integrate the version number directly in the media type. Instead, the version is passed as an option in the Accept header in the following formats:

Accept: application/vnd.awesomeapi+json; version=1

Or:

Accept: application/vnd.awesomeapi+json; version=2

Note that although we could simply use application/json, using a custom media type allows us to explain the format in detail in our documentation. We will see later in this book how to use more expressive media types based on JSON to add hypermedia controls to our documents.

For now, let’s see how we can implement this with Sinatra.

Implementation

The code is similar to the previous implementation except that this time, as API developers, we have decided to enforce the use of only one media type. Anything else will trigger a 406 error and will return the only supported media type (application/vnd.awesomeapi+json) and the available versions (1 or 2). Since we are nice, we also included what the Accept header is supposed to look like.

First, create a new file for this brand new web API.

touch webapi_media_type_version.rb

Our hash of supported media types, and the actions to take for a specific version, can be seen below. We could add more supported media types, like application/xml or application/hal+json, but to do this, we should really extract that logic into a specialized class. To keep it simple, we only support application/vnd.awesomeapi+json.

v1_lambda = lambda { |name, data| data }
v2_lambda = lambda do |name, data|
  { full_name: "#{data[:first_name]} #{data[:last_name]}", age: data[:age] }
end

supported_media_types = {
  'application/vnd.awesomeapi+json' => {
    '1' => v1_lambda,
    '2' => v2_lambda
  },
}

Below you can see the unsupported_media_type! method that we created as a helper. It will return 406 with a JSON document describing the supported media type.

helpers do
  def unsupported_media_type!(supported_media_types)
    content_type 'application/vnd.awesomeapi.error+json'
    error = supported_media_types.each_with_object([]) do |(mt, versions), arr|
      arr << {
        supported_media_type: mt,
        supported_versions: versions.keys.join(', '),
        format: "Accept: #{mt}; version={version}"
      }
    end
    halt 406, error.to_json
  end
end

Finally, in the /users route, all that is left to do is make a call to unsupported_media_type! if the client asked for an unsupported media type or an unsupported version. If all is well, the route will return the requested data after setting the Content-Type correctly (including the version!). Note how we extracted some of the code in a before block.

before do
  @media_type = request.accept.first
  @media_type_str = @media_type.to_s
  @version = @media_type.params['version'] || '1'
end

get '/users' do
  unless supported_media_types[@media_type_str] &&
     supported_media_types[@media_type_str][@version]
    unsupported_media_type!(supported_media_types)
  end

  content_type "#{@media_type}; version=#{@version}"
  users.map(&supported_media_types[@media_type_str][@version]).to_json
end

Here is the complete code for this Sinatra web API, showing how all the code above comes together.

# webapi_media_type_version.rb
require 'sinatra'
require 'json'

users = {
  thibault: { first_name: 'Thibault', last_name: 'Denizet', age: 25 },
  simon:    { first_name: 'Simon', last_name: 'Random', age: 26 },
  john:     { first_name: 'John', last_name: 'Smith', age: 28 }
}

v1_lambda = lambda { |name, data| data }
v2_lambda = lambda do |name, data|
  { full_name: "#{data[:first_name]} #{data[:last_name]}", age: data[:age] }
end

supported_media_types = {
  'application/vnd.awesomeapi+json' => {
    '1' => v1_lambda,
    '2' => v2_lambda
  },
}

helpers do
  def unsupported_media_type!(supported_media_types)
    content_type 'application/vnd.awesomeapi.error+json'
    error = supported_media_types.each_with_object([]) do |(mt, versions), arr|
      arr << {
        supported_media_type: mt,
        supported_versions: versions.keys.join(', '),
        format: "Accept: #{mt}; version={version}"
      }
    end
    halt 406, error.to_json
  end
end

before do
  @media_type = request.accept.first
  @media_type_str = @media_type.to_s
  @version = @media_type.params['version'] || '1'
end

get '/users' do
  unless supported_media_types[@media_type_str] &&
     supported_media_types[@media_type_str][@version]
    unsupported_media_type!(supported_media_types)
  end

  content_type "#{@media_type}; version=#{@version}"
  users.map(&supported_media_types[@media_type_str][@version]).to_json
end

Let’s see how well our API works. Start the server before sending the curl requests:

ruby webapi_media_type_version.rb

Without specifying an accepted media type:

curl http://localhost:4567/users

Output

[
  {
    "supported_media_type":"application/vnd.awesomeapi+json",
    "supported_versions":"1, 2",
    "format":"Accept: application/vnd.awesomeapi+json; version={version}"
  }
]

Asking for version 1:

curl http://localhost:4567/users \
  -H 'Accept: application/vnd.awesomeapi+json; version=1'

Output

[
  {"first_name":"Thibault", "last_name":"Denizet", "age":25},
  {"first_name":"Simon", "last_name":"Random", "age":26},
  {"first_name":"John", "last_name":"Smith", "age":28}
]

Asking for version 2:

curl http://localhost:4567/users \
  -H 'Accept: application/vnd.awesomeapi+json; version=2'

Output

[
  {"full_name":"Thibault Denizet", "age":25},
  {"full_name":"Simon Random", "age":26},
  {"full_name":"John Smith", "age":28}
]

Looking good!

Pros

  • It has the same pros as the previous versioning mechanism. It represents a good way of doing versioning and preserving the fundamental principles defined in RFCs of web technologies (HTTP, URIs, etc.).

Cons

  • It also shares most of the cons of the custom media type approach with one less caveat: adding a new version does not also require creating a new media type.

Who Is Doing It This Way?

I couldn’t find any famous web APIs using this versioning mechanism. Maybe you’ll be the first one ;).

My Humble Opinion

I really like this approach to be honest. I find it very clean since it separates versioning and media types, while keeping the URI clean and following HTTP principles. The implementation is probably the most complicated, but I think it gives a lot of flexibility for both the API and the client developers.

7.7. No Versioning?

Obviously, the best approach would be no versioning. While it is possible for some APIs to avoid versioning altogether by never introducing breaking changes, most developers will need to improve their APIs; sometimes that means breaking existing stuff to build awesome stuff on top of it.

Let’s remind ourselves why versioning is needed and when it can be avoided.

The usual scenario is this; you create a web API that gives access to geolocation data formatted in a very specific way. People start to build clients and create tightly coupled implementations that are expecting the data to always be formatted this way. Nobody can blame them, they are right to expect that! By publishing a web API, you are making a deal with anyone who uses it: “If you use our API, we promise we will not break your clients.” If you are not ready for this commitment, developers will look elsewhere. Now that we have a tight coupling between the clients and our server, and we cannot make any backward-incompatible changes anymore. That’s why people started to introduce versioning mechanisms in the first place. However, in a lot of cases, versioning can be avoided altogether simply by never breaking the existing clients.

  • If you need to add a few attributes to a JSON representation, go ahead, you won’t break anything (probably).
  • Maybe you also feel the need to change a resource URI? First question, why? Just create a new resource if you have to. Let’s say you had a users resource which should now be named guests (or whatever), just add the new resource. No need to version it since it’s an entirely new concept. If you really have to, then use hypermedia or 3xx redirects to let the client know there was a change.
  • It’s another story if you need to rename attributes. The basic advice is: don’t. :(

Let’s do an implementation of a version-less Sinatra web API. In our case, it’s not really complicated, we just add what we want to the v1 of the JSON representation.

Implementation

We’re lucky. As I said earlier, we don’t actually need to version our API to make the above changes. Let’s just add full_name to the only representation we have for users and everything should be fine!

Create a new file.

touch webapi_no_version.rb

And put the following code in it:

# webapi_no_version.rb
require 'sinatra'
require 'json'

users = {
  thibault: { first_name: 'Thibault', last_name: 'Denizet', age: 25 },
  simon:    { first_name: 'Simon', last_name: 'Random', age: 26 },
  john:     { first_name: 'John', last_name: 'Smith', age: 28 }
}

helpers do

  def present_user(data)
    {
      full_name: "#{data[:first_name]} #{data[:last_name]}",
      age: data[:age],
      first_name: data[:first_name],
      last_name: data[:last_name]
    }
  end

end

get '/users' do
  media_type = request.accept.first.to_s
  unless ['*/*', 'application/*', 'application/json'].include?(media_type)
    halt 406
  end

  content_type 'application/json'
  users.map { |name, data| present_user(data) }.to_json
end

Start the server.

ruby webapi_no_version.rb

And make this simple curl request:

curl http://localhost:4567/users

Output

[
  {
    "full_name":"Thibault Denizet",
    "age":25,
    "first_name":"Thibault",
    "last_name":"Denizet"
  },
  {
    "full_name":"Simon Random",
    "age":26,
    "first_name":"Simon",  
    "last_name":"Random"
  },
  {
    "full_name":"John Smith",
    "age":28,
    "first_name":"John",
    "last_name":"Smith"
  }
]

Duh! We should have started with this instead of trying all those versioning systems… Just kidding!

Pros

  • It’s super clean!
  • No need to deal with multiple versions, and everything that comes with it: upgrading clients, deprecating old APIs, updating all resources when a change is only needed for one, and so on.

Cons

  • Not versioning at all is something to strive for, but sadly, it will never be possible for every scenario.
  • No versioning for web APIs can seem like a utopia, something amazing that will never happen.
  • There is no way to handle breaking changes, which means you can’t break anything.

Who Is Doing It That Way?

  • Reddit
  • Flickr
  • Sendgrid
  • GitHub is aiming for it.

My Humble Opinion

While no versioning sounds awesome, I don’t see how it’s possible in some specific scenarios. However, developers should look as much as possible into not versioning everything and limiting what has to be versioned. Later in this book, when we learn more about REST and hypermedia, we will see some tools that reduce the need for versioning.

7.8. Wrap Up

It’s now time for you to pick your favorite versioning mechanism. We’ve seen quite a lot in this chapter, and I hope you found one that you like.