Architecting RESTful Rails 4 API
22 Oct 2013This 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</a>
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.