Iamvery

The musings of a nerd


Adding Versions to a Rails API

— Apr 16, 2014

Originally posted on the Big Nerd Ranch Blog.


When you create an application programming interface (API), you’re establishing a contract with everyone who uses it. This too is true for web service APIs. As soon as someone begins using an API, changes require coordination between all clients in order to prevent breakage, costing precious time and money.

Rails API versioning to the rescue: In order to allow breaking changes to an interface, we can version it so that clients may specify exactly what representation they expect for their requests. Then they are able to decide for themselves when it is time- and cost-effective to upgrade their dependency.

An Example API

Note: Each heading in this walkthrough will have one or more accompanying commits. You can work through it yourself or follow along on Github.

As a baseline for this post, we’ll consider a very simple, contrived API. This API has only one resource: /articles. You can grab a copy of the example by cloning it from Github and setting up as follows.

Clone the repo:

$ git clone [email protected]:iamvery/rails-api-example.git
$ cd rails-api-example

Checkout the repo in its “initial” state, before versions are implemented:

$ git checkout -b starting-point initial-api-implementation

Install dependencies, watch specs pass:

$ bundle install
$ bin/rspec
... 0 failures

Once you’ve got the project set up, let’s run the local server and see what the article response looks like.

Run the local rails server:

# Do this in a separate window so you can keep it around
$ bin/rails server

Make a request for the articles’ JSON representation:

$ curl http://localhost:3000/articles.json
[{"id":123,"name":"The Things"}]

Versioning is a Thing

At this point, our API has a single, unversioned articles resource. As a first step, we’ll introduce versioning as an internet media type parameter.

Namespaces (commit b21a0a91)

Namespacing is a great way to keep code organized. We’ll wrap our existing controller in a V1 module to establish our “version 1.”

Move app/controllers/articles_controller.rb to app/controllers/v1/articles_controller.rb and wrap the class in a module.

Since we don’t want to affect our URI structure for the resource, we can use the :module scope to namespace the controller and not the URI.

Route Constraint (commits 5d304f19 and 0ec91c6c)

Next we need to tell Rails how to route requests for different versioned representations. We can do this effectively by using a route constraint that checks for a version specified in the request’s accept header.

We can use this constraint to route requests based on the version specified in the request’s accept header.

With this change, we now must specify the desired version in the request’s headers to get the desired response. If your development server is still running at this point, it will probably need to be restarted.

$ curl -H "accept: application/json; version=1" http://localhost:3000/articles
[{"id":123,"name":"The Things"}]

Version 2 (commit 5bd29d0b)

Now that we have namespaces for our versioned controllers and constraints for routing, we can introduce a version 2 articles representation. Version 2 will wrap the response in a root node. This representation is not backwards compatible with the version 1 representation, thus requiring a new versioned representation.

We can request the version 2 representation as well as version 1:llows.

$ curl -H "accept: application/json; version=2" http://localhost:3000/articles
{"articles":[{"id":123,"name":"The Things"}]}

$ curl -H "accept: application/json; version=1" http://localhost:3000/articles
[{"id":123,"name":"The Things"}]

Version Representations, Not Locations

I was first tuned to this idea by a post from Steve Klabnik about REST and HTTP. Later I dug a little deeper and found a long answer on StackOverflow that goes into more detail. The prevaling sentiment is:

resource URIs that API users depend on should be permalinks

This cannot be true if the version is included in the URI, which changes over time. Using Klabnik’s suggestion, we push this knowledge into the request headers and keep URIs permanent for all future representations of our resources.

Internet Media Types

The above example deals only in the application/json media type. The idea of “versioning” doesn’t apply very well to the generic “json” type. That is, it doesn’t make good sense to say, “Here you’re seeing version 1 json, and over here we have version 2 json.” For that reaso,n it may be desirable to create a custom vendor media type to represent our app’s responses.

The Type (commit dbbf6ea7)

First we register a new type with Rails and give it a name.

Responding (commit 17b85059)

Now we can adjust our resource to respond to our custom type.

With this change we can now make requests of our custom type.

$ curl -H "accept: application/vnd.articles+json; version=2" http://localhost:3000/articles
{"articles":[{"id":123,"name":"The Things"}]}

$ curl -H "accept: application/vnd.articles+json; version=1" http://localhost:3000/articles
[{"id":123,"name":"The Things"}]

Bonus: URI Format Suffixes (commit 323fe034)

You may have noticed that I stopped using format suffixes for requests early in the post (e.g., /articles.json). This was done intentionally so that we could arrive at custom media types. It is, however, somewhat common to include such suffixes for requested resources. Unfortunately, Rails becomes confused into thinking that we’re requesting pure JSON rather than our “articles flavored JSON.” We can address this by responding to both formats.

Now we can make request including the suffix.

$ curl -H "accept: application/vnd.articles+json; version=2" http://localhost:3000/articles.json
{"articles":[{"id":123,"name":"The Things"}]}

$ curl -H "accept: application/vnd.articles+json; version=1" http://localhost:3000/articles.json
[{"id":123,"name":"The Things"}]

There is one caveat. The content type of the response will be application/json rather than our custom type. I provide a little more information in my commit notes.

Conclusion

Versioning code is a Good Thing™. It allows us to continue to extend our APIs without breaking compatibility for existing users. Introducing API versions after a release may be a little painful, but it’s doable.

comments powered by Disqus