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 a controller!
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 already self-explanatory.
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 right way.
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 URL-based resources.
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.