API design

What are some elements of good api design ?

This post presents a collection of principles I followed when developing the Sensr.net API, you can learn more about the sensr.net APIs by reading our tutorial.. This is the first of several posts that will cover some of the aspects presented here.

I'm going to go over some of the basics of API design and illustrate some of the points with example implementation in Ruby on Rails. If you're on a Rack stack, note that a nice alternative to implementing an API from the ground up is to go with Grape, an opinionated micro-framework for creating REST-like APIs in Ruby.

  1. Maintainability: api must be easy to test and the code easy to understand.
  2. Version: support multiple version
  3. Authentication: restrict access to private resources.
  4. Extendability: must be easy to add new API calls as new resources become available.
  5. Predictability: avoid surprising developers even if it means not following best pratices.
  6. Documentation: up-to-date and extensive documentation.
  7. Throtling
  8. Generating SDKs

Maintainability and code structure

How do you organize your code so the API is easy to maintain ? I needed a little more control than what Grape was offering me, so intead I went for a Presenter pattern. The presenter pattern is a way to compose a single unified object that can be utilized to properly output data for a specific purpose. This implementaiton yields flexibility, customization, and testability instead of ActiveRecord’s as_json rigidity. We basicaly setup presenters for our models and utilize them in our controllers to provide the desired structure of our JSON responses.

Code structure

Our code is now organized this way, for all version of the api, every model has a corresponding presenter and controller. This leads to a code that is nicely organized. Here, I’m adding a namespace to distinguish between internal API ‘/i/v3/’ calls and user level ones ‘/u/v3/’

app/models/user.rb
          | camera.rb
app/controllers/u/v3/base_controller.rb
                    |users_controller.rb
                    |cameras_controller.rb
               /i/v3/base_controller.rb
                    |users_controller.rb
app/presenter/u/v3/base_presenter.rb
                    |users_presenter.rb                     
                    |cameras_presenter.rb                     

Base Controllers

Let’s take a look at our controllers first. All controllers inherit from a base controller. The base controller has two purposes, encapsulate behavior common to all controllers and -very important- expose a description of the resources available, we will use this later for documentation.

class U::V3::BaseController < ApplicationController  
  respond_to :json
  attr_reader :current_user, :current_tenant
  
  # this is used for API discovery and documentation
  def resources
    render :json => U::V3::ResourcesPresenter.new.description
  end
  def users
    render :text => U::V3::UserPresenter.new.description
  end
  def cameras
    render :text => U::V3::CameraPresenter.new.description
  end
end

Controllers

Now a resource controller is fairly straighforward. It has a before_filter to validate the token and inherits from the base controller. You’ll notice that the code is well scoped and versionned. The resource is either looked up by query paramaters or in the case of the user as the owner of the API session token. Once the resource is loaded, we just call instantiate the resource presenter and call ‘render :json’ on it.

class U::V3::UsersController < U::V3::BaseController

  before_filter :set_resource_owner_by_oauth_token,  :except => :register
  oauth_required :scope => "user", :except => :register
  
  def me
    logger.info "[API Client: #{oauth.client}] with scope #{oauth.scope} and tenant  [#{oauth.identity}]"
    render :json => U::V3::UserPresenter.new(current_user), :status => :ok
  end
end

Base api presenter

class ApiPresenter
  def initialize(resource=nil,options={})
    @options = options
    if Rails.env.test? || Rails.env.build? || Rails.env.development? 
      @basepath = 'https://sensrapi.dev'
    elsif Rails.env.staging?
      @basepath = 'https://api.stagingserver.com'
    else
      @basepath = 'https://api.sensr.net'          
    end    
  end
end

User presenter

class U::V3::UserPresenter < ApiPresenter
  attr_reader :user

  def initialize(user=nil,options={})
    super
    @user = user
  end

  def description
    File.read("#{Rails.root}/app/assets/resources/u_v3_users.json").gsub("https://api.sensr.net", @basepath)
  end
  
  def as_json(options={})
    @options.merge!(options)
    data = {
      :user => {
        :id => @user.id,
        :email => @user.email,
        :name  => @user.name,
      },
      :urls => {
        :cancel => "#{@basepath}/u/v3/users/#{@user.id}/cancel",
        :update => "#{@basepath}/u/v3/users/#{@user.id}/update",
        :my_cameras => "#{@basepath}/u/v3/cameras/owned",
        :my_clips => "#{@basepath}/u/v3/clips/owned"      }
    }
    if @options[:include] == :cameras
      data[:user][:cameras] = @user.cameras.collect { |camera| U::V3::CameraPresenter.new(camera, {:include => :ftp_user}) }
    end
    data
  end
     
end

Versionning

Some of the principles I followed when developing our API

  1. Always include a version when releasing an API.
  2. Developers must specify a version when calling the API
  3. Specify the version with a 'v' prefix (ex: /u/v3/cameras.json)

As a developer, I like seeing the version of the api on the URL rather than in the HTTP header. With the code structure and the namespacing introduced above in the presenters and the controller, we can now release new versions of our API along side the old ones. We can apply our namescoping to our models, controllers, presenters, and specs.

I will talk about the other aspects of API desing in follow-up posts.

Yacin Bahi 18 February 2013
blog comments powered by Disqus