Rails 101: Routing

thnkmin
By thnkmin December 3, 2013 20:44

Rails 101: Routing

Routes define what URLs are used to access different parts of your application.

When you run the scaffold generator described in the previous article routes are setup for you automatically but they only setup the default settings, sometimes you need something a little more complex.

In this article, I will show you how routes work and how to set them up yourself. You can download a video version of this article, however the article is more up-to-date.

How Routes Work

You might have heard the phrase ‘RESTful routes’, REST stands for Representational State Transfer and basically makes use of different methods of request to process different actions, this way applications can interact with one another very easily over the standard HTTP protocol.

So how does REST affect you? Well if you are familiar with HTML you will know that forms have a method attribute which is set to GET or POST, however there are actually a few more methods available to HTTP but not included in HTML, these include the PUT and DELETE methods, REST uses these to figure out whether you are trying to create, read, update or delete an item.

POST /users => Create GET /users/1 => Read (show) PUT /users/1 => Update DELETE /users/1 => Delete (destroy)

The standard Rails setup needs a few more actions than these, for starters there is the index action which lists all the items in the collection, this can be accessed by GETing the collection URL, in this example at/users. Other methods by default suffix the collection URL or object URL with their name, however this behavior can be overridden – but we’ll look into that later.

GET /users => Index GET /users/new => New GET /users/1/edit => Edit

Putting The Magic To Work

The routes file is located within the config directory, by default it contains just two routes, both of which aren’t restful and simply act as catch all for non-nested URLs, personally I like to remove these as they let you get away with not setting up your routes properly.

To make our RESTful users section, all you need to enter is map.resources :users, pretty simple right? Rails uses this one command to produce a set of around 14 routes!

You can now test this with a rake command: rake routes.

If you look carefully, you’ll notice that each route has a name associated with it, such as new_user, this becomes useful in our views and controllers where we need to reference the URLs dynamically. In views you suffix the name with ‘_path’, for example new_user_path and in controllers you should use ‘_url’, for examplenew_user_url, this is because we only want the path in the view (/something), but in controllers you want the full address for use in the API (http://…).

You can setup named routes manually by typing the name of your route after map. When setting up routes manually this way you have to specify a string which is parsed for parameters, the parameters are words prefixed with a colon, like symbols in Ruby.

For an example, here’s a route to disable a user, the route will be called disable_user, and the route will include the user’s ID, and it will call the disable method of the users controller.

map.disable_user ‘users/:user_id/disable’, :controller => ‘users’, :action => ‘disable’

As you can see, ‘user_id’ prefixed with a colon, this means it’s a parameter and can be reference using params[:user_id]. You can also constrict the parameter using a regular expression, so if we only wanted to accept numbers:

map.disable_user ‘users/:user_id/disable’, :controller => ‘users’, :action => ‘disable’, :user_id => /\d/

Remember I mentioned those additional HTTP methods before? Well we can constrain by them too, by adding the conditions hash to the end containing only one key value pair, I’ll choose “PUT” which describes updating a current record.

map.disable_user ‘users/:user_id/disable’, :controller => ‘users’, :action => ‘disable’, :user_id => /\d/, :conditions => { :method => :put }

If you are going down the RESTful route (no pun intended), you generally won’t be writing your own routes like this very often, most of the time you can add them to the map.resources, so if we return back to our users entry, we can add our disable method to the member hash, which means it acts on one object, not the whole collection. If we wanted a disabled action to list all disabled users we can add it to the collection hash.

map.resources :user, :member => { :disable => :put }

Nested Routes

Another bonus for using map.resources is you can easily nest resources, so for example if a user has a profile, I would nice to be able to have routes like /users/1/profile, and to do this we can just change the current map.resources into a block and drop another map.resource within it, but you don’t use map this time, you use the word between the goal posts, in this case ‘users’.

map.resources :user, :member => { :disable => :put } do |users| users.resource :profile end

We can even refactor this further using the :has_one option:

map.resources :user, :member => { :disable => :put }, :has_one => :profile

There is also a similar option for has_many.

You can now access the profile using more route helpers such as: edit_user_profile_path(user_id)

Special Notes

There are a few special things to note about routes, and those are the root route (again, no pun intended), path prefixing, namespacing, route globbing and shallow routing.

Root URLs

When a user goes to the root URL of your site you often want users to be greeted with a homepage, or dashboard, so to do that simply create a named route called root without a URL string attached to it. In this example, I’ll make it point to the index of the users controller.

map.root :controller => ‘users’, :action => ‘index’

Path Prefixes

Path prefixing allows you to prefix resource URLs with a string such as ‘account’, to make /account/users, this means that you can group various URLs under one section.

map.resources :user, :member => { :disable => :put }, :path_prefix => ‘account’

Namespacing

If you have a bunch of controllers that have duplicate names but differing functionality, for example having a front-end ‘notes’ controller which only lists and displays notes, and a back-end one which is protected and has a different layout, then namespacing keeps things tidy.

You will need to group your routes within a namespace block as below:

namespace(:admin) do |admin| map.resources :user # … end

And your controller will have to be namespaced too:

Admin::NotesController < ApplicationController

Route Globbing

For route globbing, or in other words catch all routes with an unlimited nesting depth (i.e /random/url/here), you can prefix the parameter with an splat (asterisk) instead of a colon and in your controller and view the parameter will return an array of the parts.

map.page ‘*path’, :controller => ‘users’, :action => ‘page’

Shallow Routing

Recently introduced into Rails is the :shallow option, this allows you to access nested routes as if they were belonging to the root. For example the routes:

map.resources :notes, :shallow => true do |note| note.resources :comments end

Would allow you to use the routes comments_path and comment_path(1) rather than having to use the nested paths note_comments_path(note_id) ornote_comment_path(note_id, 1).

Summary

To create RESTful routes, you can use map.resources or resource, depending on whether you want a collection, or singular resource respectively. You can use the :member or :collection options to map additional routes within the resource, :has_many or :has_one to specify sub-routes and :path_prefix to add string prefixes to your paths.

For the homepage you should use map.root which negates the URL option, as it defaults to an empty string.

And for other named routes you simply type map. followed by the name and the URL as a string, for example map.user ‘users/:id’ then follow it with a hash of options for :controller, :action and any constraints.

thnkmin
By thnkmin December 3, 2013 20:44