Hi. I'm Jon. I'm the newest Rails dev at PatientsLikeMe. I've done plenty of web development before, but this is my first time working with Rails. In this post I'm going to talk about my first attempt to do something non-trivial with routes.

The task sounded straightforward. We had a set of pages that were already developed and deployed. But we wanted to also show them on a separate tab elsewhere, with a different set of URLs. So most of the work would be taking place in routes, with the controllers recycling a handful of views that already existed. Here's how it went.


Denial

So routes.rb is where I map a url to a controller's action? This is gonna be cake.

Anger

Where did my URL helper methods go? I need those.

Bargaining

Okay, I'll trade you a resources for a collection? Can I have my helper method back now? I'll bring you cookies...

Anger again

Why can't you find my controller anymore!? It was right there a minute ago!

The bargaining/anger loop continued for several days...

Depression

Maybe I'm not cut out for this.

Acceptance

Hey, Amy, Andy, Michael, and Nat. Can I have some help with routes? Please? I'll bring you cookies...


Self deprecation aside, I did eventually get through the routes.rb file. Once it was set up correctly, as opposed to almost correctly, I found my controller working the way I wanted it to, with fewer hoops to jump through. I expected that dealing with routes would be trivial, with the meat of the code tasks lying in the controller. I did not realize that not only were the changes inside the file pretty subtle, but they each affected several other things that I wouldn't know would be affected right away. Powerful voodoo that routes file is.

Anyway, here is what I've learned. I was disappointed with the documentation I found online so hopefully this will make life easier for somebody else out there.

First off, here are the routing methods we use: get, post, match, resource, resources, member, collection, namespace, and scope. And we modify them with these options: only, controller, and as.

Get, match, and post

get and post are self evident. match is too for that matter but it seems a little less clean. As far as I can tell we're using match when there's no single verb for an action which probably means the action isn't very well defined or we're abusing it with shenanigans.

match '/path' => 'controller#action', via: [:get, :post]
get '/path' => 'controller#action'
post '/path' => 'controller#action'

These all get you routes to /path on controller creatively named controller. Its action should be self evident. These are basic, so we're moving on.

These are the basic routing commands for routing one url to one action. I'd encountered them previously. I blame them for misleading me about the complexity of routing in rails.

Resource vs resources

Time to come clean. I've been doing this for almost 6 months and this is the first time I noticed these were two distinct methods. They behave similarly and they look very similar.

Use resource/resources for routing your CRUD operations. 99% of the time these functions will do 99% of what you want done with minimal typing. I think my standard operating procedure will be to assume I need one of these until I find a case where it doesn't work and only then think about alternatives.

That said, I'm still not 100% sure of the difference between the two. I think it has to do with the pluralization of a controller's name. resources also gives you helper methods that end in _index. I didn't like that, so I wrote off resources as useless. I'm also pretty sure that at some point I got bit because even though I was aware of resource/resources, I wasn't necessarily pluralizing the model I was referring to correctly. Ugh. Let's look at another example and see what the differences are.

resource :one_fish
resource :two_fishes
resources :red_fish
resources :blue_fishes

That's a pair of calls to each of resource and resources, each pair having a singular and plural item.

Here's what rake routes gets out of the resource calls (abbreviated to only show items with helpers):

Helper Path Controller#Action
one_fish /one_fish one_fishes#create
new_one_fish /one_fish/new one_fishes#new
edit_one_fish /one_fish/edit one_fishes#edit
two_fishes /two_fishes two_fishes#create
new_two_fishes /two_fishes/new two_fishes#new
edit_two_fishes /two_fishes/edit two_fishes#edit

No surprises there. That's unexpected. Looks like resource doesn't do any different magic when you use a singular or plural - it just sticks with your preference.

Let's try resources.

Helper Path Controller#Action
red_fish_index /red_fish red_fish#index
new_red_fish /red_fish/new red_fish#new
edit_red_fish /red_fish/:id/edit red_fish#edit
red_fish /red_fish/:id red_fish#show
blue_fishes /blue_fishes blue_fishes#index
new_blue_fish /blue_fishes/new blue_fishes#new
edit_blue_fish /blue_fishes/:id/edit blue_fishes#edit
blue_fish /blue_fishes/:id blue_fishes#show

That's a bit better. First thing I notice is we get a different set of helpers. New and edit look about the same. We've also gained an index and a show helper. The show helper is just the name of the controller (singular). Depending on whether you provide a plural controller, you may get a plural index helper or a helper with _index at the end.

I think the logic here is that resource is pointing to a single thing. If you're implementing a web based email client, you'll only ever need one inbox, so the inbox's route will be defined with resource. But it'll show many messages, so the messages will be routed with resources.

Collection and member

collection and member confused me at first but I think I've got a handle on them now. They're for actions that live on a controller but aren't the traditional CRUD actions that resource will know about. The difference is that member expects an object id. So if you were to forget about resource and write some of its actions out, index would be in collection, and edit would be in member.

I've dumped collections and members into the next example, but I don't think it's worth looking at their routes. They serve different enough purposes.

Namespace and scope

Finally, we get to namespace and scope. I didn't touch scope so I'll refrain from commenting on it. Both items seem to exist to specify a chunk of url and a block where that bit of url is going to be present.

What I didn't realize about namespace was that was actually describing a namespace for the controller (and presumably the model, but I wasn't dealing with those for this story). So this snippet:

namespace :foo do
  resource :bar
end

was looking for Foo::BarController. In retrospect this makes sense and I probably could have skipped an anger loop or three.

I think scope is doing what I expected namespace to do - prepend a token to the url path without changing the controller. Not positive though, so let's inspect another snippet.

  namespace :namespace do
    resource :resource do 
      collection {get :alpha, :bravo}
      member {get :charlie, :delta}
    end
  end

  scope :scope do
    resource :resource do 
      collection {get :alpha, :bravo}
      member {get :charlie, :delta}
    end
  end

rake routes | grep resource gets us:

Helper Path Controller#Action
alpha_namespace_resource /namespace/resource/alpha namespace/resources#alpha
bravo_namespace_resource /namespace/resource/bravo namespace/resources#bravo
alpha_resource /scope/resource/alpha resources#alpha
bravo_resource /scope/resource/bravo resources#bravo

Hmm. That wasn't so bad. I didn't remember that namespace would also stick the namespace into helpers, which might be nice on a larger application but I'd hate to type when it wasn't really necessary.

Routing options

Okay, so now that that's all laid out, let's tweak it. Our routes.rb file makes use of :only, :as, and :controller to adjust all those rules.

:only applies to resource(s) and serves to limit the verbs the resource(s) call gives you. I think this makes a lot of sense and see no reason to avoid using it.

I want to like :as as well. It lets you control the names of your helpers. I've tweaked some earlier examples to illustrate this:

resources :blue_fishes, as: :pink_fishes

namespace :namespace do
  resource :resource, as: :not_a_fish_at_all do 
    collection {get :alpha}
  end
end
Helper Path Controller#Action
pink_fishes /blue_fishes blue_fishes#index
new_pink_fish /blue_fishes/new blue_fishes#new
alpha_namespace_not_a_fish_at_all /namespace/resource/alpha namespace/resources#alpha
namespace_not_a_fish_at_all /namespace/resource namespace/resources#create

These uses of :as change the helper's name to use the as parameter instead of the controller name. They do not remove the namespace.

I like the convenience of this, but I'm not actually sure I want to use it. Why not? Because :as made it a whole lot harder to reason about what was happening when I made other changes. If I changed resource to resources, :as overrode whatever changes might have shown up. But then I remove the :as and those other changes would catch up with me. That wasn't a fun surprise.

:controller is :as as applied to a controller instead of a helper. What I mean by that is that it lets you use a controller of a different name than the one in the route. We make use of in several instances, but I haven't been able to figure out why. When I asked a more senior dev, he told me :controller was naughty and should be avoided because it makes it harder to figure out which controller you should work with. Rails has conventions for a reason and rails development is quicker when you know them. Why make someone fumble around by checking the routes file for a controller name when they don't have to. For that reason (plus laziness!) I'm omitting a :controller example as I don't want to encourage its use.


That concludes my experience with routes. I hope this is useful to someone else. I also hope that when I revisit this topic for reference in a couple months, it still makes sense and doesn't leave me wondering why the hell was in those cookies at the top of the post.