Let's take a look at our routes as they are now, using rake routes
:
$ rake routes
Prefix Verb URI Pattern Controller#Action
list_posts GET /list_posts(.:format) application#list_posts
GET /show_post/:id(.:format) application#show_post
new_post GET /new_post(.:format) application#new_post
create_post POST /create_post(.:format) application#create_post
GET /edit_post/:id(.:format) application#edit_post
POST /update_post/:id(.:format) application#update_post
POST /delete_post/:id(.:format) application#delete_post
POST /create_comment_for_post/:post_id(.:format) application#create_comment
POST /list_posts/:post_id/delete_comment/:comment_id(.:format) application#delete_comment
list_comments GET /list_comments(.:format) application#list_comments
One of the key insights for Rails conventions is that data-centric apps (or CRUDy apps) tend to have very similar ways for letting users interact with the application, which are:
Those seven "interaction patterns" are so common, that Rails provides a strong set of conventions from routing to controllers, views and forms to help make those workflows simpler. We're going to focus on the routing part now.
For routing, the convention is that instead of us having to come up with the URL patterns, Rails recommends that we structure the URLs and corresponding controller actions in the following way:
Workflow | HTTP VERB | PATH | Controller Action |
---|---|---|---|
Show a list of posts | GET | /posts | posts#index |
Show a post | GET | /posts/:id | posts#show |
Show the page to create post | GET | /posts/new | posts#new |
Create a post | POST | /posts | posts#create |
Show the page to edit a post | GET | /posts/:id/edit | posts#edit |
Update a post | PUT/PATCH | /posts/:id | posts#update |
Delete a post | DELETE | /posts/:id | posts#destroy |
We can change the post
part of our current routing to follow this convention:
# posts
GET /list_posts -> GET /posts posts#index
GET /show_post/:id -> GET /posts/:id posts#show
GET /new_post -> GET /posts/new posts#new
POST /create_post -> POST /posts posts#create
GET /edit_post/:id -> GET /posts/:id/edit posts#edit
POST /update_post/:id -> PUT/PATCH /posts/:id posts#update
POST /delete_post/:id -> DELETE /posts/:id posts#destroy
And in the routes.rb
file,
Rails.application.routes.draw do
### posts ###
get '/posts' => 'posts#index'
get '/posts/new' => 'posts#new'
get '/posts/:id' => 'posts#show'
post '/posts' => 'posts#create'
get '/posts/:id/edit' => 'posts#edit'
patch '/posts/:id' => 'posts#update'
put '/posts/:id' => 'posts#update'
delete '/posts/:id' => 'posts#destroy'
# comments later..
...
end
Notice that with this new pattern, there's a lot less verbs in the URL itself, but we're relying on the HTTP verbs (GET
/POST
/PUT
/PATCH
/DELETE
) to supply the semantics for otherwise same URL patterns, for example /posts/:id
.
Note that PUT
had been used for updating resources until recently, when PATCH
was determined to be a better semantic match for this action. Since then, Rails has moved to preferring PATCH
, but allowing both verbs for updates.
Another change we'll see later on is that we will be routing all URLs related to posts to a PostsController
instead of handling everything in ApplicationController
. The PostController
's action names
are also conventions corresponding to the 7 interaction patterns.
This is the strongest set of conventions that Rails imposes on you as an application developer. We'll ask you to make an effort to memorize it. We'll revisit this plenty of times throughout the book to help you do that as well.
Oftentimes we need to have multiple resources present in the URL. For example for our blog app whenever we need to have a route to create a comment, it's very important that we know which post this comment is created on.
We're currently doing this:
post 'create_comment_for_post/:post_id' => 'application#create_comment'
The Rails convention of a URL with multiple associated resources is to start with the "containing" resource all the way to the "innermost" resource. For example, our routes for comments would change to:
post '/posts/:id/comments' => 'comments#create'
delete '/posts/:post_id/comments/:id' => 'comments#destroy'
get '/comments' => 'comments#index'
The semantics of those URLs represent: "Create a comment under a specific post" and "delete a comment under a specific post". The last route, "show all comments in the system", doesn't need to be "scoped" under a post, so the URL doesn't have to lead with "/posts".
If we need to have multiple resources in the URL with their IDs, the convention is such that the last resource will use the placeholder :id
and all the rest will be :resource_id
such as :post_id
.
REST stands for "Representational State Transfer". It's a software architecture style and guideline to create scalable web services. REST, as a way to structure web applications, has many facets to it, but let's just look at how it applies to the interface that we use to interact with web apps, the URLs.
To see how REST simplifies the semantics of URLs, let's walk through an example. Let's say we're building an online pizza ordering service. A naïve implementation of the interfaces of the service (URLs) would be:
/check_order?order_id=2
/place_new_order
/pizza_details?type=1
/pay_for_my_order?order_id=12
/cancel_order?order_id=3
/expedite_order?order_id=13
A RESTful interface design would look like this:
GET /orders/2
POST /orders
GET /pizzas/1
POST /orders/12/payments
DELETE /orders/3
POST /orders/13/expeditions
You can see, RESTful interfaces center around "nouns" or "resources", removing "verbs" from the interfaces but instead relying on the standardized HTTP verbs (GET
/POST
/PUT
/DELETE
) to supply CRUD semantics for the actions. Verbs such as check
, place
, cancel
are mapped directly to HTTP verbs of GET
to retrieve, POST
to create, and DELETE
to destroy resource order
. pay
and expedite
are structured as creation (HTTP POST) of sub resources of payments
and expeditions
under resource orders
.
RESTful interfaces standardized web application interaction patterns to make them easier to understand and program against. Because the interfaces are centered around manipulating resources, the responses are much more predictable. An analogy of how the RESTful architecture pattern simplifies interaction with web services would be how the Relational Database and SQL language simplifies data storage. Before Relational Database and SQL, data was typically stored in proprietary systems with specific logic to interact with them, this meant that on every project you'd have to learn a different set of instructions to interact with data. Relational Databases and SQL gave a common data structure (data records as rows and columns stored in tables) and a set of common interfaces - four simple verbs of SELECT
, INSERT
, UPDATE
and DELETE
that can support all types of applications.
Adopting a RESTful interface for your web app will also streamline your application to align with how the backend data is stored in databases, and makes development easier. We'll see that in the following chapters.
We have talked about Rails' routing conventions, and RESTful URL patterns. Now our routes.rb
file would look like the following:
### config/routes.rb ###
Rails.application.routes.draw do
### posts ###
get '/posts' => 'posts#index'
get '/posts/new' => 'posts#new'
get '/posts/:id' => 'posts#show'
post '/posts' => 'posts#create'
get '/posts/:id/edit' => 'posts#edit'
patch '/posts/:id' => 'posts#update'
put '/posts/:id' => 'posts#update'
delete '/posts/:id' => 'posts#destroy'
### comments ###
get '/comments' => 'comments#index'
post '/posts/:id/comments' => 'comments#create'
delete '/posts/:post_id/comments/:id' => 'comments#destroy'
end
In fact, the routes like what we have in the posts
section are very common, so Rails provides a special resources
notation that helps generate those for you.
We can simply do:
resources :posts
This line will automatically generate the 8 lines we had before. Note that for the update
action, Rails allows both PUT
and PATCH
verb, for compatibility reasons.
You can run bundle exec rake routes
in your command line environment to verify that it generates exactly the same set of routes.
Note that this single line will generate 8 routes. In our case we happen to need all the routes here, but if you only need a subset of the routes, you can define them more explicitly as:
resources :posts, only: [:new, :create, :index]
This line above would only generate 3 routes for you - type bundle exec rake routes
to check it out.
Rails also allows us to nest resources to create URL patterns with multiple resources, see below:
### config/routes.rb ###
Rails.application.routes.draw do
resources :posts do
resources :comments, only: [:create, :destroy]
end
resources :comments, only: :index
end
Run bundle exec rake routes
and see the output - it should be the same as what we had before.
When you run bundle exec rake routes
, notice the Prefix
column for some of these routes. These are here to help us know how to use Rails' provided path helpers for our routes. We can use these prefixes followed by _path
in our controllers and views to easily build paths.
For example:
posts_path # => '/posts'
post_id = 123
post_comments_path(post_id)
# => '/posts/123/comments'
What's the advantage of using URL helpers, rather than hard coding the paths such as /posts/123/comments
directly in your code? (for example, in your controller action?) The reason you want to use path helpers is that if you want to change the URL patterns in your app, it's a lot easier to use the URL helpers to return a different path - let's look at an example:
get '/register', to: 'users#new', as: 'register'
When we add the "as" clause to this route, if you run bundle exec rake routes
, you'll see the Prefix
column with register
, and you can use register_path
to get /register
the path. Now, if we want to change the path to /login
, all we have to do is just to:
get '/login', to: 'users#new', as: 'register'
Now our register_path
gives us /login
. We don't have to change our code in the Rails app at all.
Before we move on, let's take a quick look at a couple variations of what we've already seen, as well as some other features available to us in Rails routing:
### config/routes.rb ###
Rails.application.routes.draw do
# pointing our homepage (root path) to posts#index
root to: 'posts#index'
# `match` & `via:` allow us to define one route and use several HTTP verbs
# `as:` lets us define the name of the route prefix
match '/authors/:id' => 'authors#update',
via: [:put, :patch],
as: :update_author
# update_author PUT|PATCH /authors/:id(.:format) authors#update
resources :posts do
# define extra params to pass for requests to a route
get 'popular', on: :collection, action: :index, popular: true
# popular_posts GET /posts/popular(.:format) posts#index {:popular=>true}
get 'preview', on: :member
# ...
end
# we can even use routes to redirect
get '/home', to: redirect('/')
end
Let's break down all the features listed above one at a time.
root: root
is used to specify what action maps to "/"(the top-level URL of our site which has no path).
The root URL of your website/app is the most commonly used, because of this root
should be specified
at the top of the routes file.
match + via: match
is used to match URL patterns to one or more routes. We can also specify which HTTP verb may be used for matching to a URL with this pattern. We do this by passing in a hash to the match
method. The key for this hash is via
, and the value is an array of HTTP verbs. match
is a more general purpose form of some more commonly used HTTP routing methods, such as get
, post
, and delete
. It may take all of the same options as those methods, but, compared to those methods, it gives a bit more flexibility.
For instance, by using match
we may specify a URL that matches for two different routes, each corresponding to two different HTTP verbs. Normally, doing this would require two commands, instead of one. We can see this from our example above, where we match the URL pattern '/authors/:id'
to the action 'authors#update'
. We then specify what HTTP verbs may be used to issue the request for that URL with via: [:put, :patch]
. Always specify an HTTP method when using match
, not doing so may have negative implications on your application's security.
match
presents another option for routing to certain actions in Rails. In general, it is best to stick with the more commonly used HttpHelpers
methods, such as get
and post
.
as: We mentioned a bit earlier that the option as:
may be used with our route declaration to change
the prefix for our URL helpers. This can be used in various ways. We can use as:
to make our URL helpers better match custom URLs. Another use is to change the current URL path helper to something more intuitive or something that matches up better with the resources within an application.
collection route: See how above we nest our /popular
route under the posts
resource. We then use on: :collection
to specify what part of a resource we are nesting this route under. We have many posts so that is considered a collection. By specifying on: :collection
we are saying, "Match a URL with path /posts/popular
." If we didn't add on: :collection
, Rails will assume that this route corresponds to another resource associated with a single member of our posts collection. In that case our route would become /posts/:id/popular
. We can also specify multiple collection routes by using a block format.
collection do
get 'popular'
end
params
hash when we are matched with certain URLs. There are two ways to specify this; one way is the same way we do above:
get 'popular', on: :collection, action: :index, popular: true
popular: true
will be passed to our action that the popular route matches with via the params
hash. It will have a key of :popular
and a value of true
. We can also use more explicit syntax by passing a hash to our route method get
, where
the key is defaults:
and the value is another hash containing the name of the parameter we want to pass to our action.
get 'popular', on: :collection, action: :index, defaults: { popular: true}
member route: We can use on: member
to specify that our route matches a member of a collection. If we use this the dynamic segment will show up as :id
. A URL helper will also be created as preview_post
. By specifying this route with on: member
, we are telling Rails that this is a member of this particular resource. That it isn't just another resource nested under posts, but more closely linked or related to our posts in this application. Multiple member routes may be defined using a block format; this format uses the same syntax as the block format for defining multiple collection routes, just replace collection
with member
.
redirect: There is one other concept to talk about that is displayed in our example above, and that is redirection. Rails gives us the capability to redirect from one path to another by using a redirect helper in conjunction with a route.
get '/home', to: redirect('/')
. If someone tries to access the path /home
, they will immediately be redirected
to the root path /
. This isn't restricted to just one URL path of course; we could also have something like this: get '/home', to: redirect('/travel')
.
Rails routes stand in the front of an application. A route interprets an incoming HTTP request and:
params
in controller actions
resources
macro to generate RESTful routes very quickly.