Architecting RESTful Rails 4 API

This is a follow-up to my previous post about Authentication with Rails, Devise and AngularJS. This time we’ll focus more on some aspects of the API implementation. One thing to keep in mind, where my previous example used Rails 3.2, I’m using Rails 4 this time around.

Versioning

If you are building an API which could be potentially consumed by many different clients, it’s important to version them to provide backward compatibility. That way clients can catch-up on their own time while newer versions are rolled out. Here’s what I’ve found to be the most recommended approach:

/config/routes.rb

namespace :api, defaults: {format: :json} do
  scope module: :v1, constraints: ApiConstraints.new(version: 1, default: :true) do
     devise_scope :user do
	match '/sessions' => 'sessions#create', :via => :post
	match '/sessions' => 'sessions#destroy', :via => :delete
     end
  end
end

So what we are doing there is wrapping our routes with an API namespace which will give us some separation from the admin and client-side routes by giving them their own top level /api/ segment. But to avoid being too verbose we’ll leave the version number out of the URLs. So instead of a namespace we’ll turn the version module into a scope block. Although this raises a question – how can the client request a specific version? Well that’s where something called constraints comes in.

/lib/api_constraints.rb

class ApiConstraints
  def initialize(options)
    @version = options[:version]
    @default = options[:default]
  end

  def matches?(req)
    @default || req.headers['Accept'].include?("application/vnd.myapp.v#{@version}")
  end
end

This constraint basically says if the client wants anything besides the default version of our API (in this case v1) they can send us an Accept header indicating that.

Next lets take a look at our controller.

/app/controllers/api/v1/sessions_controller.rb

class Api::V1::SessionsController < Devise::SessionsController
    protect_from_forgery with: :null_session, :if => Proc.new { |c| c.request.format == 'application/vnd.myapp.v1' }

    def create
      warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#failure")
      render :json => { :info => "Logged in", :user => current_user }, :status => 200
    end

    def destroy
      warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#failure")
      sign_out
      render :json => { :info => "Logged out" }, :status => 200
    end

    def failure
      render :json => { :error => "Login Credentials Failed" }, :status => 401
    end
end

Nothing fancy there. Pretty much what I covered in my previous post i.e overriding the default Devise controller for more control.

Documenting

Having an API means you’ll have clients which will need to know how to consume it. I know die hard HATEOS advocates will say that a REST API should be discoverable by nature. But in most real world scenarios that may not always be the case. So we’ll need to find a way to write our own documentation. Writing documentation manually would be extremely time consuming and unmaintainable. So the best way would be to somehow generate it automatically. There is a perfect gem written with just this intent by the good folks at Zipmark called rspec_api_documentation. It leverages rspec’s metadata to generate documentation using acceptance tests.

Install this gem and run:

rake docs:generate

It will automatically pickup all the passing tests in the /rspec/acceptance/* folder to generate documentation. Here’s an example of a test for the sessions controller. The key here is to use the custom DSL provided by the gem to give some context and structure to the documentation.

/spec/acceptance/api/v1/sessions_spec.rb

require 'spec_helper'
require 'rspec_api_documentation/dsl'

resource 'Session' do
  header "Accept", "application/vnd.myapp.v1"

  let!(:user) { create(:user) }

  post "/api/sessions" do
    parameter :email, "Email", :required => true, :scope => :user
    parameter :password, "Password", :required => true, :scope => :user

    let(:email) { user.email }
    let(:password) { user.password }

    example_request "Logging in" do
      expect(response_body).to be_json_eql({ :info => "Logged in",
                                         :user => user
                                       }.to_json)
      expect(status).to eq 200
    end
  end

  delete "/api/sessions" do
    include Warden::Test::Helpers

    before (:each) do
      login_as user, scope: :user
    end

    example_request "Logging out" do
      expect(response_body).to be_json_eql({ :info => "Logged out"
                                       }.to_json)
      expect(status).to eq 200
    end
  end
end

By default it will generate HTML files in the /docs/ folder. If you want more control over the output, there is an option to generate JSON files which can then be rendered by another gem such as raddocs or your own home brewed solution. Just specify the output format in your spec_helper file.

/spec/spec_helper.rb

RSpec.configure do |config|
.
.
   RspecApiDocumentation.configure do |config|
       config.format = :json
       config.docs_dir = Rails.root.join("docs", "")
   end
.
.
end

References

My sample code is based upon many recommendations found in the Rails 3 in Action book and it’s github repo

I also plan to update the RADD example with Rails 4 and the code shown here as soon as I get some time. RADD has now been upgraded to Rails 4 along with the versioning & documentation techniques shown here.

  • aljohri

    Thanks for posting this! Please do update the RADD example as soon as you can!

  • aljohri

    “I’ve based my thinking heavily on the logic from jes.al: authentication with devise and angular, but having got this working I’ve found that the existing devise controllers seem to handle json just fine, whereas jes.al seem to have had problems with redirects. I think this change added relatively recently to devise, within the devise github page we can review the controller code itself for the session controller and see that there is logic for “is_navigational_format” – which I think means it doesn’t do redirects when the request format is json. So, whilst we’re using jes.al for inspiration, we’ve written our own code that should require no changes to native devise at the rails end.”

    http://technpol.wordpress.com/2013/09/23/angularjs-and-devise-authentication-with-a-rails-server/

  • http://nepalonrails.tumblr.com Millisami

    Thanks for this. It became an awesome help for me. Though I m not using Devise, the rspec-api-doc part was really helpful.

  • jesalg

    Glad you found this useful!

  • jesalg

    Your article was pretty in-depth and interesting. Thanks for the mention!