Starkiller
programming and such
Posted on 17 March 2011
The company I work for has included an HTTP API along with our core Rails product for a couple of years now. While we originally started with just a few generic API methods to give customers an alternative interface to our data, we quickly realized that we needed to take great care in the development and maturation of our API. Our customers helped us to identify three critical areas they needed assurance with when using our API:
  1. A guarantee that the API methods will always behave as defined and will be unchanging
  2. Thorough documentation describing how to execute each method and what data the method returns / receives.
  3. Error handling to cope with improperly formed requests by client applications
I'll be addressing each of these points in a series of posts following a sample project on Github. To address the problem of guaranteeing the integrity of the API, we need to use an API versioning scheme.

Why Use A Versioned API?

Ever since David Heinemeier Hansson's HTTP lovefest release of Rails, the Rails project has been adding changes that make building out a resource-based API simpler. What has been lacking, however, is a built-in way to enable the versioning of API method calls. Versioning an HTTP API allows for the API developer to incrementally make changes to API methods that would otherwise break existing API clients. An API version is a specification of the API at a particular point in time; a client built against an API version can be guaranteed to work even when successive versions of the API are introduced. The approach described below is meant to be a starting point for versioning an API served by a Rails 3 project. I have intentionally left out the details pertaining to content negotiation and other important API topics that should be handled so that I could focus specifically on the aspect of versioning.

A Versioned URI Design

Nenad Nikolic states in this Stackoverflow question:
The topic of URI design is at the same time the most prominent part of a REST API and, therefore, a potentially long-term commitment towards the users of that API.
While designing to include versioning in our API's URI, we want to achieve the following:
  1. Provide clients the ability to bind to a specific API version with a low-impact version included in the URI
  2. Provide clients the ability to bind to the latest API version with a version-less URI scheme
To achieve this, we will provide both a versioned and version-less API scheme. Assuming we currently have two versions of the API, both of the following URIs refer to version 2 of the called method:
/api/droid/1
/api/v2/droid/1
The following URI refers to version 1 of the called method:
/api/v1/droid/1
Note that this example API does not provide provisions for method deprecation or redirection due to deprecation. Maybe I'll cover that in a future update...

Getting Started

To begin building a versioned API, start a new example project and generate a base model and API controller:
> rails new api-example
> cd api-example
> rails generate model Droid
> rails generate controller Api
> rails generate controller Api::Droid
Let's also set up a simple table and pre-populate it with some data:
db/migrations/20110314182816_create_droids.rb
class CreateDroids < ActiveRecord::Migration
  def self.up
    create_table :droids do |t|
      t.string :name
    end
  end

  def self.down
    drop_table :droids
  end
end
db/seeds.rb
Droid.create(:name => "C-3PO")
Droid.create(:name => "R2-D2")
> rake db:migrate
> rake db:seed
Next we'll set up a couple of routes to support the API methods we'll be creating. I chose to break the HTTP API into its own API namespace, so we can take advantage of Rails 3 scoped routing to provide this namespace:
config/routes.rb
ApiExample::Application.routes.draw do
  scope 'api(/:version)', :module => :api, :version => /v\d+?/ do
    get 'droids'      => "droid#list",  :as => "api_droid_list"
    get 'droids/:id'  => "droid#view",  :as => "api_droid_view"
  end
end
With the api(/:version) route, we achieve our goal of providing both a versioned and a version-less URI scheme, as the :version parameter is optional.

Creating an API Method

Now that we have the routes created, let's fill in the API methods! We separate out our versioned API methods by adding a _# suffix to each action in the controller, where '#' is the version of the API call. This way we can have list_1 and a list_2 methods to represent two separate versions of the same API method. We'll get into how to wire up the ApiController to properly call versioned methods in the next section, but for now, let's add the two methods we've created routes for in the DroidController:
app/controllers/api/droid_controller.rb
class Api::DroidController < ApiController
  def list_1
    render :json => Droid.all
  end

  def view_1
    droid = Droid.find_by_id(params[:id])
    if droid.nil?
      render :json => {:error => "Droid #{params[:id]} not found"}, :status => 404 and return
    end
    render :json => droid
  end
end
Pretty simple, right? These two methods are now registered to API version 1 (and subsequent versions by default). Let's assume that after publishing our API, however, we didn't like how the default Droid JSON rendered. We shouldn't just go change the list_1 method, because clients may have already been building against this published version of the API. Instead, we should introduce a new action method, list_2:
def list_2
  render :json => Droid.all.map {|d| d.as_json(:version => 2)}
end
Since we are directly invoking the as_json method on the Droid model, we should override that method as well to behave differently when the :version parameter is set.
app/models/droid.rb
class Droid < ActiveRecord::Base

  alias_method :orig_as_json, :as_json
  def as_json(options = {})
    base = orig_as_json
    if options[:version] == 2
      base = {'name' => self.name}
    end
    base
  end

end
With this accomplished, we should now receive two differently formatted results when we call /api/v1/droids and /api/v2/droids. Versions 1 and 2 of the view method should be identical, since we didn't modify the method between versions.

Setting up ApiController

The entirety of our API method invocation logic resides in the ApiContoller class (in app/controllers/api_controller.rb). This acts as a base class from which all other API controllers inherit from and provides automatic registration of API methods. To act as a an API method registry, we set up a couple of class variables and define a few class methods.
app/controllers/api_controller.rb
@@versions = {}
@@registered_methods = {}

class << self
  alias_method :original_inherited, :inherited
  def inherited(subclass)
    original_inherited(subclass)

    load File.join("#{Rails.root}", "app", "controllers",
                   "api", "#{extract_filename(subclass)}")

    subclass.action_methods.each do |method|
      regex = Regexp.new("^(.*)_(\\d+)$")
      if match = regex.match(method)
        key = "#{subclass.to_s}##{match[1]}"
        @@versions[match[2].to_i] = true
        @@registered_methods[key] ||= {}
        @@registered_methods[key][match[2].to_i] = true

        subclass.instance_eval do
          define_method(match[1].to_sym) {}
        end
      end
    end
    subclass.reset_action_methods
  end

  def extract_filename(subclass)
    classname = subclass.to_s.split('::')[1]
    parts = classname.underscore.split('_')
    "#{parts.reject{|c| c == 'controller'}.join('_')}_controller.rb"
  end

  def reset_action_methods
    @action_methods = nil
    action_methods
  end
end
The inherited method is called whenever the ApiController class is inherited by an API subclass. This method will load the API subclass and parse through the actions, extracting the method names and versions, and add each method to the registry. The inherited routine also adds an empty method for each API method declared; this ensures that the Rails router will appropriately find each method that is called and will pass the request along to the controller object. When the controller receives a request to process an action, a poorly-documented method called process_action is called. We override this method to intercept the action invocation, check the version requested, and map the appropriate action to be called by the original process_action method. The code for this is below, along with a helper method.
alias_method :original_process_action, :process_action
def process_action(method_name, *args)
  method = "no_api_method"
  if protected_actions.include? method_name
    method = method_name
  else
    if params[:version]
      params[:version] = params[:version][1,params[:version].length - 1].to_i
    else
      params[:version] = @@versions.keys.max
    end
    method = find_method(method_name, params[:version])
  end
  original_process_action(method, *args)
end

def find_method(method_name, version)
  key = "#{self.class.to_s}##{method_name}"
  versions = @@registered_methods[key].keys.sort

  final_method = 'no_api_method'

  versions.reverse.each do |v|
    if v <= version
      final_method = "#{method_name}_#{v}"
      break
    end
  end

  final_method
end
And that's it! By laying just a little foundation code, it is pretty simple to start versioning method calls for your API controllers. The full source for the ApiController class can be found on Github here.

What's Next

The above solution deals specifically with the retrieval of data through an API but fails to address the special handling of API methods that accept data as an argument. I will address data acceptance and validation in a future post in addition to introducing a framework for automatically generating versioned API documentation.