13 May 2008
•
Software Development
In my previous post on this subject I described an approach to versioning the API of a REST/HTTP web service. This approach has significant advantages over the approach that is currently most common (i.e. embedding a version token in the URL). However, it does have some downsides. This post is an attempt to outline those and to present some ways to mitigate the negative impacts.
Using content negotiation to manage versions requires, by definition, the introduction of nonstandard media types. There is really no way around this. I personally don’t feel this is a Bad Thing. The new, nonstandard, media types do a much better job describing the sort of media the client is requesting. It does, however, mean that browsers – and perhaps some HTTP tools – will work less well with the web service.
The browser not working is a pretty big issue. They are almost certainly not the target consumer of the services, but having the browser not work raises the level of effort for exploring the API. If you have created a cool new service you want as few barriers to entry as possible. Personally, I always use curl when I am exploring but I know several people who would prefer to use a browser.
Unfortunately, I don’t really have a great general solution for browsers. That being said, in many situations a much can be done to make life better. For example, if the resources in question do not have HTML representations you could serve the current preferred format with a generic content type that browsers can render – e.g. text/plain
or application/xml
– to browsers.
Curl
One advantage of having the version token directly in the URL is that it makes it really easy to use curl against the service. By default curl makes requests with the Accept header field set to */*
. For a reasonably designed service this would result in a response in the current preferred format. If you want to change to Accept header you need to invoke curl like this
curl --header 'Accept: application/vnd.foo.myformat-v1+xml' http://api.example/hello-world
That is not too horrible, really. It is a bit much to type all the time, but I have curl rc files for all the formats I deal with on a daily basis. If your service is implemented in Rails there is an even easier way. With Rails you give each format you support a short name that may be used as an “extension” for URLs. For example, if we define the short name for application/vnd.foo.myformat-v1+xml
to be mf1
we can say this
curl http://api.example/hello-world.mf1
That is equivalent, from the point of view of a Rails based service, to the previous example. I imagine similar functionality could be implemented in most web frameworks. This effectively puts you back to having the version embedded in the URL, which is convenient for debugging and exploration. (It is still unsuitable for production use, though, for all the same reasons as other approaches to embedding the version in the URL.)
Nonobviousness
Another potential downside of using content negotiated versioning is that the various versions my be less discoverable, compared to a version-in-the-URL approach. I am not entirely sure this is true – after all there is a version token in the media type – but if it is true it would be a Good Thing.
Do you really want people “discovering” a version of the API that was deprecated a year ago? I think it might be better, in either approach, to use version tokens that are not readily guessable. Obviously, previous versions of and API will be documented and remain accessible, but raising some barriers to entry on depreciated parts of a system seems appropriate to me.
Unfamiliarity
This may be the biggest issue of all. People are just not very familiar, and therefore comfortable, with content negotiation. This in spite of the fact that it has been a fundamental part of HTTP since forever. I think this features obscurity is waning now, though, because it is such a powerful feature.
Two years ago Rails got content negotiation support. (That link seems to be broken at the moment. You can see part of the post I am talking about by going here and searching for “The Accept header”.) As frameworks like Rails keep adding and improving their support for this powerful feature the community of developers will become more familiar and comfortable with it. What is needed now is more education in the community on how best to utilize this feature.
Related Posts
If you’re interested in REST/HTTP service versioning be sure not to miss the rest of the series.
12 May 2008
•
Software Development
Managing changes to APIs is hard. That is no surprise to anyone who has ever maintained an API of any sort. Web services, being a special case of API, are susceptible to many of the difficulties around versioning as other types of APIs. For HTTP based REST style web services the combination of resources and content negotiation can be used to mitigate most of the issues surrounding API versioning.
Let’s assume you have a REST/HTTP web service that has some account resources. Say you can make a request like this:
===>
GET /accounts/3 HTTP/1.1
Accept: application/vnd.mycompany.myapp+xml
<===
HTTP/1.1 200 OK
Content-Type: application/vnd.mycompany.myapp+xml
<account>
<name>Inigo Montoya</name>
</account>
First, you probably noticed that my example uses a vendor MIME media type to describe the representation. Using a more generic MIME media type like application/xml
is much more common, at least in my experience. Using generic media types is perfectly legal but a bit silly. You are not really asking for any old XML document, but rather an XML document that has a quite specific schema. Aside from my idealistic rantings, using a specific media type has some strong practical benefits which are at the core of this post.
Backwards compatible changes
Often changes will need to be made to expose new behavior of the system that do not negatively impact correctly implemented clients. Say, for example, you want to start tracking email address for accounts. If the application/vnd.mycompany.myapp+xml
format documentation is clear that elements that are not recognized should be ignored you can simply add a email element to the account representation.
<account>
<name>Inigo Montoya</name>
<email-address>mailto:prepare-to-die@youkilledmyfather.example</email-address>
</account>
Any client that was created before the addition of the email element will simply ignore it’s presence. Problem solved.
Incompatible changes
Unfortunately, not all changes can be implemented in a way that is backwards compatible. For example, a couple of months after adding email to accounts the sales team sign a deal for 1 bazillion dollars. But the new customer demands that each account be allowed to have more than one email address. After thinking for a while, you decide that the best way to expose that is by changing the account representation as follows.
<account>
<name>Inigo Montoya</name>
<email-addresses>
<email-address priority='1'>mailto:prepare-to-die@youkilledmyfather.example</email-address>
<email-address priority='2'>mailto:vengeance@youkilledmyfather.example</email-address>
<email-address>
</account>
That, of course, will break any clients that are expecting the old format – so pretty much all of them. This is a place where we can bring content negotiation to bear. You can simply define a new media type – say application/vnd.mycompany.myapp-v2+xml
– and associate new multi-email format with it. Clients can then request whichever format they want. Older clients don’t know the new media type so they get served the older single email format.
===>
GET /accounts/3 HTTP/1.1
Accept: application/vnd.mycompany.myapp+xml
<===
HTTP/1.1 200 OK
Content-Type: application/vnd.mycompany.myapp+xml
<account>
<name>Inigo Montoya</name>
<email-address>mailto:prepare-to-die@youkilledmyfather.example</email-address>
</account>
Newer clients do know the new media type so they can have access to the new functionality.
===>
GET /accounts/3 HTTP/1.1
Accept: application/vnd.mycompany.myapp-v2+xml
<===
HTTP/1.1 200 OK
Content-Type: application/vnd.mycompany.myapp-v2+xml
<account>
<name>Inigo Montoya</name>
<email-addresses>
<email-address priority='1'>mailto:prepare-to-die@youkilledmyfather.example</email-address>
<email-address priority='2'>mailto:vengeance@youkilledmyfather.example</email-address>
<email-address>
</account>
Everyone gets what they need. Easy as pie.
Alternate approaches
The most commonly proposed approach for versioning REST/HTTP web service interfaces today seems to be to mutilate the URIs by inserting a version. For example,
http://foo.example/api/v1/accounts/3
I really hate this approach as it implies that an account in one version of the API is really a different resource than the account in a different version of the API.
It also forces clients into a nasty choice, either support multiple versions of the API simultaneously or break one of the core constrains of REST. For example, say a client exists for the v1 API that saves references (URIs that include the version indicator) to accounts. Some time later the client is updated to support the new version of the API. In this situation the The client can support both versions of the API simultaneously because all the previously stored URIs point at the old version of the API or it has to mung all the URIs it has stored to the point at the new API. Munging all the URIS breaks the HATEOAS constraint of REST and supporting multiple versions of the API is a maintenance nightmare.
Conclusion
Changes to REST/HTTP web service interfaces come it three basic flavors, changes to the properties associated with a type of resource, additions of new types of resources and deprecation of obsolete types of resources. If you are following the HATEOAS constraint of REST the approach described here can be used to safely handle all three scenarios.
This approach does lead to media types being created, but media types are cheap so we can – and should – have as many as we need. Used properly, content negotiation can be used to solve the problems related to versioning a REST/HTTP web service interface.
Related Posts
If you’re interested in REST/HTTP service versioning be sure not to miss the rest of the series.
11 May 2008
•
Personal
Today I said to Elliot, “on average …” (What I was talking about is not very interesting. Trust me.) Of course, Elliot immediately asked what average is. It turns out that describing average to a four year old is really hard.
My first inclination was to describe the function that is used to calculate the arithmetic mean of set of data. Elliot does not understand division yet so that was right out. Instead, I started to describe median. Median is easy to describe to a four year old and, as it turns out.
Before I had gotten very far into describing median I decided that I should probably answer the question Elliot has actually asked. So I finished describing median, then told him that what I had described was something call median which is one way of calculating average. Then I took a moment to think about average some more.
At that point, I realized that probability is the important part of average. I decided that a four year old could understand some basic probability. So, I describe how I am of average height and if we guessed that some man we had never meet was my height we would be correct more often than not because more people are of average height than any other height.
I felt like, while that description was true (particularly for the imprecise way four year olds figure height) it left out some important aspects of average. However, by this time Elliot was thoroughly bored with the subject so I let it go. I still don’t feel like I have a decent elevator pitch for average, and understanding average seems really important. For me, these sorts of situations are some of the hardest (and most interesting) parts of parenting.
06 May 2008
•
Software Development
There is big news from the work front, SystemShepherd 5.0 has been released. This is what I work on day-to-day and it is great to see it finally officially released.
SystemShepherd is a seriously cool bit of software designed to help you manage your software-as-a-service offering more effectively. If you have a system that you need to manage better you owe it to yourself to check it out.
01 May 2008
•
Software Development
Today I was working at my local coffee shop1. I notice a guy near me with a large “Python Powered” sticker on his laptop. I wandered over, said “hi” and asked if he got to use Python daily basis.
He replied, “Yes. I have my own company so I get to use what ever technology I like.” Nice.
We ended up having a short discussion about the similarities and differences of Python and Ruby, my language of choice these days. Some what surprisingly, for me at least, was that he found the tendency to omit parenthesis in Ruby code one of the most unsettling things about reading Ruby. That is one of the bits of syntactic sugar I like best in Ruby. Now that I have had it pointed out to me I can see his point of view, though. I suppose it is all what you are use to.
I was just really happy to meet another person on the tech fringe. I really need a super cool “tech fringe” sticker for my laptop so that I too can announce to the world – or at least all the geeks nearby – I am out on the fringes of technology, where the fun is.