A rails newbie explores the routes file and five stages of grief
Jul 21, 2014 by Jon SagotskyHi. 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.