Building a fast, lightweight REST service with Rails 3

September 18, 2010 Pivotal Labs

I recently started building a Rails 3 app that will function as an internal REST service. I wanted it to be as lightweight and fast as possible, both to test and to run. Here are a few ways I configured the app to be a bit faster:

  • Defined a limited route set
  • Removed ActiveResource
  • Removed unnecessary middleware
  • Created a custom controller that inherits from Metal

First, I defined the public api for my app in the routes file. I chose not to use resources because

  • I don’t need named routes
  • I wanted the :id parameter for the show action to be named :account_id instead of id (a pet peeve of mine with Rails routing)
  • I only needed 3 of the routes generated by resources

The routes file ended up looking like this:

scope "/api/v1" do
  scope "/accounts" do
    post "/" => "accounts#create"
    get ":account_id" => "accounts#show"
    get ":account_id/transactions" => "accounts#transactions"
    put ":account_id" => "accounts#update"
  end
end

Next, to get rid of ActiveResource, in application.rb I deleted the line the requires rails/all and replaced it with:

# config/appliction.rb
require "rails"

%w(
  active_record
  action_controller
  action_mailer
).each do |framework|
  begin
    require "#{framework}/railtie"
  rescue LoadError
  end
end

Rails has a lot of middleware that I didn’t need for a stateless service like this. This app will be run as a single-threaded app, behind a firewall, with no session or cookies, no views, it will only be accessed via an HTTP library and I’ll likely never open it in a browser even locally. As such, I added this to my application file:

# config/application.rb
[
  Rack::Sendfile,
  ActionDispatch::Flash,
  ActionDispatch::Session::CookieStore,
  ActionDispatch::Cookies,
  ActionDispatch::BestStandardsSupport,
  Rack::MethodOverride,
  ActionDispatch::ShowExceptions,
  ActionDispatch::Static,
  ActionDispatch::RemoteIp,
  ActionDispatch::ParamsParser,
  Rack::Lock,
  ActionDispatch::Head
].each do |klass|
  config.middleware.delete klass
end

# config/environments/production.rb
config.middleware.delete ActiveRecord::ConnectionAdapters::ConnectionManagement

I was surprised by that last one. It’s a rack middleware class sitting in ActiveRecord whose sole purpose is to not close connections in test mode. It seems to me that Rails should just include that class in environments/test.rb, as opposed to having it in every environment. It probably doesn’t matter much, since it’s only going to add a few nanoseconds to each request.

I didn’t need any view rendering at all, so I deleted app/views and app/helpers, then created my own ApplicationController class with just the modules I needed:

class ApplicationController < ActionController::Metal
  include AbstractController::Logger
  include Rails.application.routes.url_helpers
  include ActionController::UrlFor
  include ActionController::Rendering
  include ActionController::Renderers::All
  include ActionController::MimeResponds
end

In my case this was the minimum set of modules necessary to make my tests pass. Unfortunatley, these modules are still very poorly documented, so to figure out which ones I needed I had to:

  • Copy all the code from ActionController::Base into my ApplicationController
  • Delete modules one by one, testing after each one was removed and only leaving in the ones that broke the tests when they were removed

In my controller I just used render :json => object, as opposed to the new respond_to because I don’t really care to check the accept header for every request. For now there will only be json responses, regardless of what you ask for, so I didn’t bother with adding the additional overhead. Here’s what an action looks like:

class AccountsController < ApplicationController
  def show
    render :json => Account.find(params[:account_id])
  end
end

I normally use cucumber for integration testing, but for an app like this I decided to use rspec 2’s new request spec format, which is lighter but still exercises the stack. It looks something like this:

# spec/requests/api_spec.rb
require 'spec_helper'
describe "api" do
  describe "GET /api/v1/accounts/:account_id" do
    it "returns a json hash with the proper data" do
      get "/api/v1/accounts/abc123"
      Yajl::Parser.parse(response.body).should == { "id" => "abc123", "billing_date" => "12/12/2009" }
    end
  end
end

In the end, this has translated to slightly faster load times, faster test runs and faster responses in production without altering the things I like best about Rails.

About the Author

Biography

More Content by Pivotal Labs
Previous
New Account Management Features in Pivotal Tracker
New Account Management Features in Pivotal Tracker

We're introduced new features in Pivotal Tracker to help with account management. Accounts tend to stay out...

Next
Tracker maintenance tonight at 8:30pm PDT
Tracker maintenance tonight at 8:30pm PDT

We will be performing a maintenance update to Pivotal Tracker tonight (Friday, Sep 17) starting at 8:30pm P...

How do you measure digital transformation?

Take the Benchmark