API Versioning in Rails with Accept HTTP headers
To implement API versioning in Rails, not using URL namespaces but custom MIME types, there are a few different approches.
The Tribesports way
Recently, I’ve seen a blog post about the Tribesports API. They chose to add a new MIME type to the application and use the rendering features of Rails.
Maybe I’ve not seen all the constraints they might have, and that lead to this choice, but from my point of view, 2 things are bothering me :
- they had to change too much things in the rendering process
- they created the
api_v1content type but in fact it’s plain JSON
What if they wanted to render JSON or XML but for the version 1 of their API ?
With a simple protected method in the ApplicationController, it’s possible to inspect the
Accept HTTP header, and extract an API version number, while letting Rails decide what is the real content type to use.
class ApplicationController < ActionController::Base respond_to :html, :xml, :json protected def api_version default_version = '1' pattern = /application\/vnd\.com\.example\.api\.v([\d.]+)+.*/ request.env['HTTP_ACCEPT'][pattern, 1] || default_version end end
In this example, by default all the action method of all controllers will respond to HTML, XML and JSON, and Rails is probably using the default HTML if nothing is specified. I can even implement a rendering for another format in the respond_to block of a action method, like format.js for Ajax requests…
We now have a method, accessible from all controller methods (including actions) to get the desired API version nummber. If an “application/vnd”-style
Accept header is found and if a version number can be extracted, then it is used. There is a fallback to the default version.
With this, you can have any test you want at the controller level on the API version, without messing with the content type.
About the default version number, some prefer the lastest (and allow clients to set a specific version), and some prefer the first version. I prefer the “latest by default” way.
NB : I’m quite sure I’ve not made this one up, but I honestly can’t remember where I’ve read this from.
andrewmcdonough 2011-10-07 07:55:38
Using this method, how would you handle the different as_json/to_json methods required by new API versions, without ending up with really bloated models, and without having to add lots of conditional logic to your controller methods?
Jérémy Lecour 2011-10-07 08:48:49
I’d rather use the Presenter pattern than as_json/to_json methods.
With API you often have a resource embedding a collection of su-resources, than also can be fetch on their own. WIth the Presenter pattern you can define how you want your data to be arranged, then you just have to format them as you want (html, json, xml, …)
SimonC 2011-10-07 10:35:30
Thanks for responding to our post - interesting points.
Firstly, I disagree that JSON on its own is the “real” content type. For me, the notation is just one part of the content type. I can construct two entirely different representations of the same resource that are both perfectly valid JSON; are these really the same content type? I’d argue not; a client expecting one will not be able to make use of the other. Making Rails aware of the distinction at the response/renderer layer seems the sensible way to deal with this, particularly as hooks are provided precisely for this purpose.
If we want to provide an XML version of our API, then we just define a second Mime type with a ‘.api.v1+xml’ suffix. We’d have to modify our decorators to construct the underlying representation before rendering it in JSON/XML, but that’s not too onerous (it’s something we may well do if we ever release our API publically).
Also, when you say “let Rails decide the real content type” - it’s not going to be capable of doing this unless you register your Mime type as we did. If your ApplicationController were as you describe, the result of a call with any unregistered Accept string would be 406 Not Acceptable. You could (thanks to a bit of Rails oddness) override the Accept header by adding a format extension, e.g.
$ curl -v -H 'application/vnd.example.api.v1+json' 'http://example.com/posts.json'
Here, Rails will completely ignore the Accept header (!) and respond using the basic json renderer. Then you could inspect the Accept header as you describe, without it interfering with Rails’ choice of renderer. But it’s not quite clear to me how you would then use your controller method to select the appropriate representation for Rails to render - at least, not without adding repetitive code to every controller action included in your API, which is something we were very keen to avoid in our app.