2 June 2010

Rethinking Rails 3 Controllers and Routes

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: index, show, new, create, edit, update and delete. Their 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 the HTTP 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

    * From Webstock 2009

    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.

    Method URL Action Helper
    GET /pancakes index pancakes_path
    GET /pancakes/:id show pancake_path(:id)
    GET /pancakes/new new new_pancake_path
    POST /pancakes create pancakes_path
    GET /pancakes/:id/edit edit edit_pancake_path(:id)
    PUT /pancakes/:id update pancake_path(:id)
    DELETE /pancakes/:id destroy pancake_path(:id)
    # 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.

    Controllers should generate and cache assets, not view helpers.

    Controllers should generate and cache assets, not view helpers.

    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.

    Conclusion

    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.

    Reactions

    Thanks to Reg Braithwaite and Ben Hoskings for feedback on this article.

    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.).

    The 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.

    Thoughts and reactions? Reply to @topfunky on Twitter, write a blog post of your own, or comment at Hacker News.

    1 Video of Reg Braithwaite at RubyFringe. Quote is about 15:30 in.

    2 The original version by David Heinemeier Hansson, then by Ulysses, then by Koz and Jamis Buck (together), and most recently by Yehuda Katz, Josh Peek, and the Rails 3 team.

    3 Sample controller code. Versioning and expiration of assets are easily solved by storing a version number in the application and appending it to the link tag.

    4 See hammock by Ben Hoskings, make_resourceful by Hampton Catlin and resource_controller by James Golick.

    5 Carl Lerche’s Astaire plugin.