Rethinking Rails 3 Controllers and Routes
Pluralsight blog Where devs, IT admins & creative pros go for news, tips, videos and more.
3,500+ tech & creative courses authored by experts - unlimited & online Get it now →
June 2, 2010

Rethinking Rails 3 Controllers and Routes

By

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.

About the Author

is VP of Open Source at Pluralsight. He previously founded PeepCode and is an all around entrepreneur, developer, designer, teacher and athlete. Follow him on Twitter at @topfunky.


Discussion