Rails Routing Basics
recognizes HTTP request verbs and URLs
dispatches requests to the right controller and action
provides methods for paths and URLs for the application
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
Rails uses these:
Others:
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 |
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) |
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
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 |
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
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 |
It might be tempting to nest deeply, but this creates more problems and confusion than it solves.
Keep nesting to 1 level.
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
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 |
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 |
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
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 |
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 |
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"
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.
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)