Rails Routing

Rails Routing Basics

What is routing?

recognizes HTTP request verbs and URLs

dispatches requests to the right controller and action

provides methods for paths and URLs for the application

What is an HTTP request?

From the RFC:

Request = Request-Line
          *(( general-header | request-header | entity-header ) CRLF)
          CRLF
          [ message-body ]
Request-Line = Method SP Request-URI SP HTTP-Version CRLF
GET / 1.1

HTTP Methods

Rails uses these:

  • GET
  • POST
  • PUT (aka PATCH)

Others:

  • OPTIONS
  • HEAD
  • TRACE
  • CONNECT

Rails Routing Resources

Rails standard routing by default
uses the concept of a resource.

resources :photos

Which creates 7 different routes:

Verb + Path Controller#Action
GET /posts posts#index
GET /posts/new posts#new
POST /posts posts#create
GET /posts/:id posts#show
GET /posts/:id/edit posts#edit
PUT /posts/:id posts#update
DELETE /posts/:id posts#destroy

Note:

Because the router uses the HTTP verb and the URL to match the route, four URLs can map to seven different actions.

URL Verb
/posts GET (index), POST (create)
/posts/:id GET (read), PUT (update), DELETE (destroy)
/posts/:id/new GET (new)
/posts/:id/edit GET (edit)

Choosing the Controller

Rails determines which controller it should map to by the following convention: convert the resource symbol to a string, run camelize on it, concatenate the string “Controller”, and then constantize that.

(resource_symbol.to_s.camelize + "Controller").constantize

Thus:

resources :posts # => PostsController
"posts#new" # => PostsController#new

Path and URL Helpers

The third thing that Rails routing does is provide helper methods for the paths and URLs associated with a given controller and action.

assume @post.id == 10

Helper Method Result
posts_path /posts
posts_url https://example.com/posts
edit_posts_path(@post) /posts/10/edit
edit_posts_url(@post) https://example.com/posts/10/edit

Nested Resources

Resources are logically nested, with children and parents. In our classic model, Comments and the children of Posts.

class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

Nesting routes captures this relationship:

resources :posts do
  resources :comments
end

Resulting Nested Routes

path verb url action
post_comments GET /posts/:post_id/comments(.:format) comments#index
  POST /posts/:post_id/comments(.:format) comments#create
new_post_comment GET /posts/:post_id/comments/new(.:format) comments#new
edit_post_comment GET /posts/:post_id/comments/:id/edit(.:format) comments#edit
post_comment GET /posts/:post_id/comments/:id(.:format) comments#show
  PATCH /posts/:post_id/comments/:id(.:format) comments#update
  PUT /posts/:post_id/comments/:id(.:format) comments#update
  DELETE /posts/:post_id/comments/:id(.:format) comments#destroy

Limits to Nesting

It might be tempting to nest deeply, but this creates more problems and confusion than it solves.

Keep nesting to 1 level.

Shallow Nesting

A useful way to avoid deep nesting, yet keeping many of the benefits.

resources :posts do
  resources :comments, only: [:index, :new, :create]
end
resources :comments, only: [:show, :edit, :update, :destroy]

Rails provides a shorthand for the above:

resources :posts do
  resources :comments, shallow: true
end

Resulting Shallow Routes

Prefix Verb URI Pattern Controller#Action
post_comments GET /posts/:post_id/comments(.:format) comments#index
  POST /posts/:post_id/comments(.:format) comments#create
new_post_comment GET /posts/:post_id/comments/new(.:format) comments#new
edit_comment GET /comments/:id/edit(.:format) comments#edit
comment GET /comments/:id(.:format) comments#show
  PATCH /comments/:id(.:format) comments#update
  PUT /comments/:id(.:format) comments#update
  DELETE /comments/:id(.:format) comments#destroy

Additional RESTful Actions

You can add more actions to a resource by adding routes to the collection or individual members.

resources :posts do
  resources :comments, shallow: true
  collection do
    get 'search'
  end
  member do
    get 'review'
  end
end
Prefix Verb URI Pattern Controller#Action
search_posts GET /posts/search(.:format) posts#search
review_post GET /posts/:id/review(.:format) posts#review

Namespacing

The concept of “namespacing” goes across Rails in several ways. Namespacing routes will allow you to place a resource under a common path, and is often used in conjunction with namespaced controllers.

In a namespaced controller, you’ll have a parent module and the controller class:

class Admin::UsersController < Admin::BaseController

In routing, you’d follow suit:

namespace :admin do
  resources :users
end

Resulting Namespaced Routes:

Prefix Verb URI Pattern Controller#Action
admin_users GET /admin/users(.:format) admin/users#index
  POST /admin/users(.:format) admin/users#create
new_admin_user GET /admin/users/new(.:format) admin/users#new
edit_admin_user GET /admin/users/:id/edit(.:format) admin/users#edit
admin_user GET /admin/users/:id(.:format) admin/users#show
  PUT /admin/users/:id(.:format) admin/users#update
  DELETE /admin/users/:id(.:format) admin/users#destroy

Restricting the Routes Created

Normally Rails creates the seven routes for the default controller actions (index, show, new, create, edit, update, destroy) when you specify a resource.

You can restrict the routes by using the
keywords only and except

resources :comments, only: [:index, :show]
Prefix Verb URI Pattern Controller#Action
comments GET /comments(.:format) comments#index
  GET /comments/:id(.:format) comments#show

Making SEO-Friendly Routes

Sometimes you want to make your routes more SEO-friendly, in that they reflect the name of what you’re trying to use.

The simplest way doesn’t involve changing your routes file, at all. Set the to_param class method in your model, and the path/URL helpers will automatically use it.

class Post < ActiveRecord::Base
  to_param :title
end

post_path(@post) # => "/posts/2-my-little-chthulu"

Routing Matches

If the “/posts/2-my-little-chthulu” URL path is issued with a GET HTTP verb, it would match:

Prefix Verb URI Pattern Controller#Action
post GET /posts/:id(.:format) posts#show

The PostsController#show action would receive the params[:id] containing “2-my-little-chthulu”.

The controller finds the post in the set_post before action callback and loads the post with id equal to 2.

Why this works

The reason this works is pretty simple, and a little stupid: When Rails runs the .find(id) method, it first runs .to_i on the id. Since the actual id number is the first thing in the parameterized “slug”, it can determine the id:

"2-my-little-chthulu".to_i # => 2

So:

# .. in PostsController#set_post:
@post = Post.find(params[:id])
# => Post.find("2-my-little-chthulu")
# => Post.find(2)

Thanks!