Rethinking Rails 3 Controllers and RoutesBy Geoffrey Grosenbach
DISCLAIMER: Reg Braithwaite delivered this quote in a completely different context1, but I think it’s applicable to the design of almost any software API. Illustration by Mike Rohde.
The Rails router has been written and rewritten at least four
times2, including a recent rewrite for the upcoming Rails
3. The syntax is now more concise.
But never mind making it shorter! It’s time for a final
rewrite: Let’s get rid of it altogether!
Let’s get rid of the seven controller actions:
meaning is blurred in the context of REST. They’re an unhelpful
layer between programmer and protocol.
Let’s get rid of the trivial but cumbersome mental
translation you have to do everytime you want to think about
GET method, the URL, the
index action, and the
plural_path URL helper.
Let’s get rid of duplicate functionality already implemented
in the controller. If you need to redirect, take action based
on the user agent, or examine headers, that should be done in
And let’s start thinking in URLs, resources, and APIs instead of
doing image caching in models or asset bundling in
view helpers. That’s the controller’s job. It scales better, too.
- Routes are unnecessary configuration.
- The seven standard controller actions are legacy.
- Become intimate with your URLs – don’t abstract them away.
- Decrease the distance between thought and implementation.
- Let the controller do its job.
Get Back to Thinking in URLs
A big part of the problem with a routing layer is that it
abstracts the developer away from the URLs that define the
application. This leads to poor API designs and convoluted
solutions to otherwise easy problems.
This epiphany came while writing a few Sinatra
applications. The exact URL for a handler sits right in
front of my eyes as I write the code for it. I can’t ignore it.
As a result, I find myself spending more time thinking about
how my URLs are designed. Should I be serving JSON from the
same controller that serves the HTML interface, or should it
be organized separately?
In contrast, you can write an entire Rails application without
ever looking at a URL. The design of URLs is delegated to the
framework, out of sight and out of mind.
This isn’t to say that we need to lose the good parts of how
Rails works with URLs. URL helper methods like
pancake_path(:id) are a great idea for reducing duplication
and typos. They could be implemented apart from any router.
“I love URLs. I dream about them at night. I think about them before I think about anything else.” — Adrian Holovaty, co-creator of Django
Retire the Seven Action Names
It has been three years since the seven controller actions had
any immediate meaning. The API no longer adds to the
programmer’s understanding of the tasks at hand.
Experienced programmers know that inline comments mark code
that’s too confusing or too clever.
Yet every Rails controller is generated with two lines of
repetitive comments for every action. That’s a code smell! And
a failure of API design.
Every Rails developer must memorize the table at right,
mapping HTTP method and URL to the controller action name. If
we can get rid of the action name, the rest of the table is
So instead of going through this extra syntactical layer, let’s
deal directly with GET, POST, PUT, and DELETE. (Suggestions follow.)
You can tell this is a great idea because it’s the way unit tests
already work! Let’s bring this syntax back to the controller and
complete the API.
# GET /pancakes # GET /pancakes.xml def index end # POST /pancakes # POST /pancakes.xml def create end
test "should create pancake" do post :create assert_redirected_to pancake_path(1) end
Assets Are Resources
Thinking in URLs helps you solve web-related problems in the
Exhibit A: If you deploy to a site whose view fragments are
already cached in memcached, it’s likely that Rails’ asset
caching view helpers will not be called and CSS bundles will
not be generated.
# Caching shouldn't happen here stylesheet_link_tag("a", "b", "c", :cache => true)
This whole problem is easily solved by using controllers to
do what they do best. Views and helpers should not generate
The task of combining several CSS files into one file and
caching them to disk is exactly the kind of task controllers
are built to do3.
Idea 1: Sinatra
There are many ways the controller API could be
improved. Existing attempts have failed to achieve wide
adoption because they have tried to handle controllers, views,
and models all at once4.
Instead, I think a more limited, controller-only approach
could work better.
The first is already in wide use: Sinatra.
# Sinatra-style handler get "/api/v1/report/:id" do |id| # ... end
Sinatra is arguably the most widely replicated Ruby web
framework, having inspired implementations in Node.js,
Clojure, PHP, Scala, and many other languages.
Part of the problem with writing a routing API is finding a
way to describe multiple URLs in a single string. This problem
is solved if you handle each URL on its own.
There’s no middle routing layer with arbitrary action
names. HTTP method and URL are all you need (but handlers can
filter on the user agent or other header information).
Thanks to Rack, Sinatra apps can be embedded in Rails
applications. Or, Carl Lerche is writing a Rails 3 plugin that
provides this syntax to Rails controllers5.
Idea 2: Method, Member, Collection
Jamis Buck introduced an implementation of REST as a plugin
four years ago. Yet unlike other areas of Rails that have
seen massive improvements, Rails’ implementation of REST is
basically the same as it was in Rails 1.2.
A halfway approach could combine elements of the Rails 3
router syntax with the class method style of configuration
that’s already familiar to Rails developers.
# Class method and HTTP-style methods. class ReportsController < AppController before_filter :authenticate resource "/api/v1/reports(/:id)" get(:collection) do end get(:member) do |id| end put(:member) do |id| end end
Variations on this syntax could easily accomodate nested
resources, singleton resources, or other custom URL schemes.
For several years, Rails has charged forward and defined new,
innovative syntaxes for writing web applications. It’s time for
it to happen again.
An improved syntax could get rid of stale controller
actions, reduce confusion, reduce duplication, and
improve the way we think about solving technical problems
with web applications.
Ben Hoskings said
I think the mere fact that a controller can be generated means that code shouldn’t be there. There’s no intelligence in code that can be generated from a single resource name.
I think it’s doubly important because once you free the controller from having to implement the specifics of each action (like models doesn’t have to implement the specifics of
#save), you have a lot more room to raise the abstraction level and start getting declarative.
I think making the controller more declarative is probably the end goal. Instead of the bulk of each action being an imperative mess, the controller can be heavier on the class-level configuration that’s really proven itself in the model (associations, scopes, plugin configuration, etc.).
provides class-level configuration directive in Merb/Rails 3 is a signal that controllers can head in this direction.
Kyle Neath gives several useful guidelines in detail for thinking about URL design.
About the Author
- Live Coding Rails 3 Upgrade
- Ruby, Emacs & Flog Metrics
- Nginx development script for passenger
- The sound of no command
- Am I the only one who uses a text editor to edit files?
- What pythonistas think of ruby
- Windows Server 2008: Install Active Directory Domain Services
- 5 Great Things About Server 2008 — Is an Upgrade Worth It?
- Don’t Worry He Can’t Write: The Story of the RODC
- Installing WordPress on IIS7 – Part 2