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 ofid
(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