I dreamed a thousand new paths...I woke and walked my old one.
—Chinese proverb
The routing system in Rails is the system that examines the URL of an incoming request and determines what action should be taken by the application. And it does a good bit more than that. Rails routing can be a bit of a tough nut to crack. But it turns out that most of the toughness resides in a small number of concepts. After you’ve got a handle on those, the rest falls into place nicely.
This chapter will introduce you to the principal techniques for defining and manipulating routes. The next chapter will build on this knowledge to explore the facilities Rails offers in support of writing applications that comply with the principles of representational state transfer (REST). As you’ll see, those facilities can be of tremendous use to you even if you’re not planning to scale the heights of REST theorization. Both chapters assume at least a basic knowledge of the model-view-controller (MVC) pattern and Rails controllers.
Some of the examples in these two chapters are based on a small auction application. The examples are kept simple enough that they should be comprehensible on their own. The basic idea is that there are auctions and each auction involves auctioning off an item. There are users and they submit bids. That’s it.
The triggering of a controller action is the main event in the life cycle of a connection to a Rails application. Therefore, it makes sense that the process by which Rails determines which controller and which action to execute must be very important. That process is embodied in the routing system.
The routing system maps URLs to actions. It does this by applying rules that you specify using a special syntax in the config/routes.rb
file. Actually, it’s just plain Ruby code, but it uses special methods and parameters, a technique sometimes referred to as an internal domain-specific language (DSL). If you’re using Rails generators, code gets added to the routes file automatically, and you’ll get some reasonable behavior. But it doesn’t take much work to write custom rules and reap the benefits of the flexibility of the routing system.
The routing system does two things: It maps requests to controller action methods, and it enables the dynamic generation of URLs for you for use as arguments to methods like link_to
and redirect_to
.
Each rule—or to use the more common term, route—specifies a pattern, which will be used both as a template for matching URLs and as a blueprint for creating them. The pattern can be generated automatically based on conventions, such as in the case of REST resources. Patterns can also contain a mixture of static substrings, forward slashes (mimicking URL syntax), and positional segment key parameters that serve as “receptors” for corresponding values in URLs.
A route can also include one or more hard-coded segment keys, in form of key/value pairs accessible to controller actions in a hash via the params
method. A couple of keys (:controller
and :action
) determine which controller and action gets invoked. Other keys present in the route definition simply get stashed for reference purposes.
Putting some flesh on the bones of this description, here’s a sample route:
get 'recipes/:ingredient' => "recipes#index"
In this example, you find the following:
• HTTP verb constraining method (get
)
• Static string (recipes
)
• Slash (/
)
• Segment key (:ingredient
)
• Controller action mapping ("recipes#index"
)
Routes have a pretty rich syntax—this one isn’t by any means the most complex (nor the most simple)—because they have to do so much. A single route, like the one in this example, has to provide enough information both to match an existing URL and to manufacture a new one. The route syntax is engineered to address both of these processes.
Routes are defined in the file config/routes.rb
, as shown (with some explanatory comments) in Listing 2.1. This file is created when you first create your Rails application and contains instructions about how to use it.
1 Rails.application.routes.draw do
2 # The priority is based on order of creation:
3 # first created -> highest priority.
4 # See how all your routes lay out with "rake routes".
5
6 # You can have the root of your site routed with "root":
7 # root 'welcome#index'
8
9 # Example of regular route:
10 # get 'products/:id' => 'catalog#view'
11
12 # Example of named route that can be invoked with
13 # purchase_url(id: product.id)
14 # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase
15
16 # Example resource route (maps HTTP verbs to controller
17 # actions automatically):
18 # resources :products
19
20 # Example resource route with options:
21 # resources :products do
22 # member do
23 # get 'short'
24 # post 'toggle'
25 # end
26 #
27 # collection do
28 # get 'sold'
29 # end
30 # end
31
32 # Example resource route with subresources:
33 # resources :products do
34 # resources :comments, :sales
35 # resource :seller
36 # end
37
38 # Example resource route with more complex subresources:
39 # resources :products do
40 # resources :comments
41 # resources :sales do
42 # get 'recent', on: :collection
43 # end
44 # end
45
46 # Example resource route with concerns:
47 # concern :toggleable do
48 # post 'toggle'
49 # end
50 # resources :posts, concerns: :toggleable
51 # resources :photos, concerns: :toggleable
52
53 # Example resource route within a namespace:
54 # namespace :admin do
55 # # Directs /admin/products/* to Admin::ProductsController
56 # # (app/controllers/admin/products_controller.rb)
57 # resources :products
58 # end
59 end
The whole file consists of a single call to the method draw
of Rails.application.routes
. That method takes a block, and everything from the second line of the file to the second-to-last line is the body of that block.
At runtime, the block is evaluated inside of an instance of the class ActionDispatch::Routing::Mapper
. Through it, you configure the Rails routing system.
The routing system has to find a pattern match for a URL it’s trying to recognize or a parameters match for a URL it’s trying to generate. It does this by going through the routes in the order in which they’re defined—that is, the order in which they appear in routes.rb
. If a given route fails to match, the matching routine falls through to the next one. As soon as any route succeeds in providing the necessary match, the search ends.
The basic way to define a route is to supply a URL pattern plus a controller class/action method mapping string with the special :to
parameter.
get 'products/:id', to: 'products#show'
Since this is so common, a shorthand form is provided:
get 'products/:id' => 'products#show'
David publicly commented on the design decision behind the shorthand form when he said that it drew inspiration from two sources:
1) the pattern we’ve been using in Rails since the beginning of referencing controllers as lowercase without the “Controller” part in controller: "main"
declarations and 2) the Ruby pattern of signaling that you’re talking about an instance method by using #. The influences are even part mixed. Main #index would be more confusing in my mind because it would hint that an object called Main actually existed, which it doesn’t. MainController#index would just be a hassle to type out every time. Exactly the same reason we went with controller: "main"
vs. controller: "MainController"
. Given these constraints, I think "main#index"
is by far the best alternative.1
1. Full comments at http://yehudakatz.com/2009/12/26/the-rails-3-router-rack-it-up
As of Rails 4, it’s recommended to limit the HTTP method used to access a route. If you are using the match
directive to define a route, you accomplish this by using the :via
option:
match 'products/:id' => 'products#show', via: :get
Rails provides a shorthand way of expressing this particular constraint by replacing match
with the desired HTTP method (get
, post
, patch
, etc.)
get 'products/:id' => 'products#show'
post 'products' => 'products#create'
If, for some reason, you want to constrain a route to more than one HTTP method, you can pass :via
an array of verb names.
match 'products/:id' => 'products#show', via: [:get, :post]
Defining a route without specifying an HTTP method will result in Rails raising a RuntimeError
exception. While strongly not recommended, a route can still match any HTTP method by passing :any
to the :via
option.
match 'products' => 'products#index', via: :any
Keep in mind that there’s no necessary correspondence between the number of fields in the pattern string, the number of segment keys, and the fact that every connection needs a controller and an action. For example, you could write a route like the following:
get ":id" => "products#show"
This would recognize a URL like the following:
http://localhost:3000/8
The routing system would set params[:id]
to 8
(based on the position of the :id
segment key, which matches the position of 8
in the URL), and it would execute the show
action of the products
controller. Of course, this is a bit of a stingy route in terms of visual information. On the other hand, the following example route contains a static string, products/
, inside the URL pattern:
match 'products/:id' => 'products#show'
This string anchors the recognition process. Any URL that does not contain the static string products/
in its leftmost slot will not match this route.
As for URL generation, static strings in the route simply get placed within the URL that the routing system generates. The URL generator uses the route’s pattern string as the blueprint for the URL it generated. The pattern string stipulates the substring products
.
As we go, you should keep the dual purpose of recognition/generation in mind, which is why it was mentioned several times so far. There are two principles that are particularly useful to remember:
• The same rule governs both recognition and generation. The whole system is set up so that you don’t have to write rules twice. You write each rule once, and the logic flows through it in both directions.
• The URLs that are generated by the routing system (via link_to
and friends) only make sense to the routing system. The resulting URL (http://example.com/products/19201)
contains not a shred of a clue as to what’s supposed to happen when a user follows it—except insofar as it maps to a routing rule. The routing rule then provides the necessary information to trigger a controller action. Someone looking at the URL without knowing the routing rules won’t know which controller and action the URL maps to.
The URL pattern string can contain parameters (denoted with a colon), referred to as segment keys. In the following route declaration, :id
is a segment key:
get 'products/:id' => 'products#show'
When this route matches a request URL, the :id
portion of the pattern acts as a type of matcher and picks up the value of that segment. For instance, using the same example, the value of id
for the following URL would be 4
: http://example.com/products/4
.
This route, when matched, will always take the visitor to the product controller’s show
action. You’ll see techniques for matching controller and action based on segments of the URL shortly. The symbol :id
inside the quoted pattern in the route is a segment key (that you can think of as a type of variable). Its job is to be latched onto by a value.
What that means in the example is that the value of params[:id]
will be set to the string "4"
. You can access that value inside your products/show
action.
When you generate a URL, you have to supply values that will attach to the segment keys inside the URL pattern string. The simplest to understand (and original) way to do that is using a hash, like this:
link_to "Products",
controller: "products",
action: "show",
id: 1
As you probably know, it’s actually more common nowadays to generate URLs using what are called named routes versus supplying the controller and action parameters explicitly in a hash. However, right now we’re reviewing the basics of routing.
In the call to link_to
, we’ve provided values for all three parameters of the route. Two of them are going to match the hard-coded, segment keys in the route; the third, :id
, will be assigned to the corresponding segment key in the URL pattern.
It’s vital to understand that the call to link_to
doesn’t know whether it’s supplying hard-coded or segment values. It just knows (or hopes!) that these three values, tied to these three keys, will suffice to pinpoint a route, a pattern string, and therefore a blueprint for generating a URL dynamically.
Hard-Coded Parameters
It’s always possible to insert additional hard-coded parameters into route definitions that don’t have an effect on URL matching but are passed along with the normal expected params
.
get 'products/special' => 'products#show', special: 'true'
Mind you, I’m not suggesting that this example is a good practice. It would make more sense to me (as a matter of style) to point at a different action rather than inserting a clause. Your mileage may vary.
get 'products/special' => 'products#special'
Note that the treatment of the :id
field in the URL is not magic; it’s just treated as a value with a name. If you wanted to, you could change the rule so that :id
was :blah
, but then you’d have to do the following in your controller action:
@product = Product.find(params[:blah])
The name :id
is simply a convention. It reflects the commonness of the case in which a given action needs access to a particular database record. The main business of the router is to determine the controller and action that will be executed.
The id
field ends up in the params
hash, already mentioned. In the common, classic case, you’d use the value provided to dig a record out of the database:
1 class ProductsController < ApplicationController
2 def show
3 @product = Product.find(params[:id])
4 end
5 end
Rails 3 introduced a syntax for defining optional parts of the URL pattern. The easiest way to illustrate this syntax is by taking a look at the legacy default controller route, found in the previous versions of Rails at the bottom of a default config/routes.rb
file:
match ':controller(/:action(/:id(.:format)))', via: :any
Note that parentheses are used to define optional segment keys, kind of like what you would expect to see when defining optional groups in a regular expression.
It’s possible to code a redirect directly into a route definition using the redirect
method:
get "/foo", to: redirect('/bar')
The argument to redirect
can contain either a relative URL or a full URI.
get "/google", to: redirect('https://google.com/')
The redirect
method can also take a block, which receives the request params as its argument. This allows you to, for instance, do quick versioning of web service API endpoints.2
2. Examples are drawn from Yehuda Katz’s excellent blog post about generic actions in Rails 3 routes at http://yehudakatz.com/2009/12/20/generic-actions-in-rails-3/
match "/api/v1/:api",
to: redirect { |params| "/api/v2/#{params[:api].pluralize}" },
via: :any
The redirect
method also accepts an optional :status
parameter.
match "/api/v1/:api", to:
redirect(status: 302) { |params| "/api/v2/#{params[:api].pluralize}" },
via: :any
The redirect
method returns an instance of ActionDispatch::Routing::Redirect
, which is a simple Rack endpoint, as we can see by examining its source code.
1 module ActionDispatch
2 module Routing
3 class Redirect # :nodoc:
4 ...
5 def call(env)
6 req = Request.new(env)
7
8 # If any of the path parameters has an invalid encoding then
9 # raise since it's likely to trigger errors further on.
10 req.symbolized_path_parameters.each do |key, value|
11 unless value.valid_encoding?
12 raise ActionController::BadRequest,
13 "Invalid parameter: #{key} => #{value}"
14 end
15 end
16
17 uri = URI.parse(path(req.symbolized_path_parameters, req))
18 uri.scheme ||= req.scheme
19 uri.host ||= req.host
20 uri.port ||= req.port unless req.standard_port?
21
22 if relative_path?(uri.path)
23 uri.path = "#{req.script_name}/#{uri.path}"
24 end
25
26 body = %(<html><body>You are being
27 <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)
28
29 headers = {
30 'Location' => uri.to_s,
31 'Content-Type' => 'text/html',
32 'Content-Length' => body.length.to_s
33 }
34
35 [ status, headers, [body] ]
36 end
37 ...
38 end
39 end
40 end
Let’s revisit the legacy default route again:
match ':controller(/:action(/:id(.:format)))', via: :any
The .:format
at the end matches a literal dot and a “format” segment key after the id field. That means it will match, for example, a URL like the following:
http://localhost:3000/products/show/3.json
Here, params[:format]
will be set to json
. The :format
field is special; it has an effect inside the controller action. That effect is related to a method called respond_to
.
The respond_to
method allows you to write your action so that it will return different results, depending on the requested format. Here’s a show
action for the products controller that offers either HTML or JSON:
1 def show
2 @product = Product.find(params[:id])
3 respond_to do |format|
4 format.html
5 format.json { render json: @product.to_json }
6 end
7 end
The respond_to
block in this example has two clauses. The HTML clause just consists of format.html
. A request for HTML will be handled by the usual rendering of a view template. The JSON clause includes a code block; if JSON is requested, the block will be executed and the result of its execution will be returned to the client.
Here’s a command-line illustration, using curl
(slightly edited to reduce line noise):
$ curl http://localhost:3000/products/show/1.json -i
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 81
Connection: Keep-Alive
{"created_at":"2013-02-09T18:25:03.513Z",
"description":"Keyboard",
"id":"1",
"maker":"Apple",
"updated_at":"2013-02-09T18:25:03.513Z"}
The .json
on the end of the URL results in respond_to
choosing the json branch, and the returned document is a JSON representation of the product.
Requesting a format that is not included as an option in the respond_to
block will not generate an exception. Rails will return a 406 Not Acceptable
status to indicate that it can’t handle the request.
If you want to set up an else condition for your respond_to
block, you can use the any
method, which tells Rails to catch any other formats not explicitly defined.
1 def show
2 @product = Product.find(params[:id])
3 respond_to do |format|
4 format.html
5 format.json { render json: @product.to_json }
6 format.any
7 end
8 end
Just make sure that you explicitly tell any
what to do with the request or have view templates corresponding to the formats you expect. Otherwise, you’ll get a MissingTemplate
exception.
ActionView::MissingTemplate (Missing template products/show,
application/show with {:locale=>[:en], :formats=>[:xml],
:handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}.)
You’ll see usage of the :to
option in routes throughout this chapter. What’s most interesting about :to
is that its value is what’s referred to as a Rack endpoint. To illustrate, consider the following simple example:
get "/hello", to: proc { |env| [200, {}, ["Hello world"]] }
The router is very loosely coupled to controllers! The shorthand syntax (like "items#show"
) relies on the action
method of controller classes to return a Rack endpoint that executes the action requested.
>> ItemsController.action(:show)
=> #<Proc:0x01e96cd0@...>
The ability to dispatch to a Rack-based application, such as one created with Sinatra,3 can be achieved using the mount
method. The mount
method accepts an :at
option, which specifies the route the Rack-based application will map to.
1 class HelloApp < Sinatra::Base
2 get "/" do
3 "Hello World!"
4 end
5 end
6
7 Rails.application.routes.draw do
8 mount HelloApp, at: '/hello'
9 end
Alternatively, a shorthand form is also available:
mount HelloApp => '/hello'
You can also trigger a branching on respond_to
by setting the Accept
header in the request. When you do this, there’s no need to add the .:format
part of the URL. (However, note that out in the real world, it’s difficult to get this technique to work reliably due to HTTP client/browser inconsistencies.)
Here’s a curl
example that does not specify a .json
format but does set the Accept
header to application/json
:
$ curl -i -H "Accept: application/json" http://localhost:3000/products/show/1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 81
Connection: Keep-Alive
{"created_at":"2013-02-09T18:25:03.513Z",
"description":"Keyboard",
"id":"1",
"maker":"Apple",
"updated_at":"2013-02-09T18:25:03.513Z"}
The result is exactly the same as in the previous example.
Sometimes you want not only to recognize a route but to recognize it at a finer-grained level than just what components or fields it has. You can do this through the use of the :constraint
option (and possibly regular expressions).
For example, you could route all show
requests so that they went to an error action if their id
fields were nonnumerical. You’d do this by creating two routes, one that handled numerical ids and a fall-through route that handled the rest:
get ':controller/show/:id' => :show, constraints: {:id => /d+/}
get ':controller/show/:id' => :show_error
Implicit Anchoring
The example constraint we’ve been using,
constraints: {:id => /d+/}
seems like it would match "foo32bar"
. It doesn’t because Rails implicitly anchors it at both ends. In fact, as of this writing, adding explicit anchors A
and z
causes exceptions to be raised.
Apparently, it’s so common to set constraints on the :id
param that Rails lets you shorten our previous example to simply
get ':controller/show/:id' => :show, id: /d+/
get ':controller/show/:id' => :show_error
Regular expressions in routes can be useful, especially when you have routes that differ from each other only with respect to the patterns of their components. But they’re not a full-blown substitute for data-integrity checking. You probably still want to make sure that the values you’re dealing with are usable and appropriate for your application’s domain.
From the example, you might conclude that :constraints
checking applies to elements of the params
hash. However, you can also check a grab bag of other request attributes that return a string, such as :subdomain
and :referrer
. Matching methods of request
that return numeric or boolean values are unsupported and will raise a somewhat cryptic exception during route matching.
# only allow users admin subdomain to do old-school routing
get ':controller/:action/:id' => :show, constraints: {subdomain: 'admin'}
If for some reason you need more powerful constraints checking, you have full access to the request
object by passing a block or any other object that responds to call
as the value of :constraints
like the following:
# protect records with id under 100
get 'records/:id' => "records#protected",
constraints: proc { |req| req.params[:id].to_i < 100 }
At around line 8 of the default config/routes.rb
(refer to Listing 2.1), you’ll see the following:
# You can have the root of your site routed with "root":
# root 'welcome#index'
What you’re seeing here is the root route—that is, a rule specifying what should happen when someone connects to
http://example.com # Note the lack of "/anything" at the end!
The root route says, “I don’t want any values; I want nothing, and I already know what controller and action I’m going to trigger!”
In a newly generated routes.rb
file, the root route is commented out, because there’s no universal or reasonable default for it. You need to decide what this nothing URL should do for each application you write.
Here are some examples of fairly common empty route rules:
1 root to: "welcome#index"
2 root to: "pages#home"
3
4 # Shorthand syntax
5 root "user_sessions#new"
Defining the empty route gives people something to look at when they connect to your site with nothing but the domain name. You might be wondering why you see something when you view a newly generated Rails application that still has its root route commented out.
The answer is that if a root route is not defined, by default, Rails will route to an internal controller Rails::WelcomeController
and render a welcome page instead.
In previous versions of Rails, this was accomplished by including the file index.html
in the public directory of newly generated applications. Any static content in the public directory hierarchy matching the URL scheme that you come up with for your app results in the static content being served up instead of triggering the routing rules. Actually, the web server will serve up the content without involving Rails at all.
A Note on Route Order
Routes are consulted, both for recognition and for generation, in the order they are defined in routes.rb
. The search for a match ends when the first match is found, meaning that you have to watch out for false positives.
In some situations, you might want to grab one or more components of a route without having to match them one by one to specific positional parameters. For example, your URLs might reflect a directory structure. If someone connects to
/items/list/base/books/fiction/dickens
then you want the items/list
action to have access to all four remaining fields. But sometimes there might be only three fields:
/items/list/base/books/fiction
Or sometimes there might be five:
/items/list/base/books/fiction/dickens/little_dorrit
So you need a route that will match (in this particular case) everything after the second URI component. You define it by globbing the route with an asterisk.
get 'items/list/*specs', controller: 'items', action: 'list'
Now the products/list
action will have access to a variable number of slash-delimited URL fields, accessible via params[:specs]
:
def list
specs = params[:specs] # e.g., "base/books/fiction/dickens"
end
Globbing Key-Value Pairs
Route globbing might provide the basis for a general mechanism for fielding ad hoc queries. Let’s say you devise a URI scheme that takes the following form:
http://localhost:3000/items/q/field1/value1/field2/value2/...
Making requests in this way will return a list of all products whose fields match the values, based on an unlimited set of pairs in the URL.
In other words, http://localhost:3000/items/q/year/1939/material/wood
could generate a list of all wood items made in 1939. The route that would accomplish this would be the following:
get 'items/q/*specs', controller: "items", action: "query"
Of course, you’ll have to write a query
action like this one to support the route:
1 def query
2 @items = Item.where(Hash[*params[:specs].split("/")])
3 if @items.empty?
4 flash[:error] = "Can't find items with those properties"
5 end
6 render :index
7 end
How about that square brackets class method on Hash
, eh? It converts a one-dimensional array of key/value pairs into a hash! Further proof that in-depth knowledge of Ruby is a prerequisite for becoming an expert Rails developer.
The topic of named routes almost deserves a chapter of its own. In fact, what you learn here will feed directly into our examination of REST-related routing in Chapter 3, “REST, Resources, and Rails.”
The idea of naming a route is basically to make life easier on you, the programmer. There are no outwardly visible effects as far as the application is concerned. When you name a route, a new method gets defined for use in your controllers and views; the method is called name_url
(with name being the name you gave the route), and calling the method, with appropriate arguments, results in a URL being generated for the route. In addition, a method called name_path
also gets created; this method generates just the path part of the URL, without the protocol and host components.
The way you name a route is by using the optional :as
parameter in a rule:
get 'help' => 'help#index', as: 'help'
In this example, Rails will generate methods called help_url
and help_path
in controller and view contexts, which you can use wherever Rails expects a URL or URL components:
link_to "Help", help_path
And, of course, the usual recognition and generation rules are in effect. The pattern string consists of just the static string component "help"
. Therefore, the path you’ll see in the hyperlink will be the following:
/help
When someone clicks on the link, the index
action of the help
controller will be invoked.
Xavier Says ...
You can test named routes in the console directly using the special app
object.
>> app.clients_path
=> "/clients"
>> app.clients_url
=> "http://www.example.com/clients"
Named routes save you some effort when you need a URL generated. A named route zeros in directly on the route you need, bypassing the matching process that would be needed other. That means you don’t have to provide as much detail as you otherwise would, but you still have to provide values for any segment keys in the route’s pattern string that cannot be inferred.
When you create a named route, you’re actually creating at least two route helper methods. In the preceding example, those two methods are help_url
and help_path
. The difference is that the _url
method generates an entire URL, including protocol and domain, whereas the _path
method generates just the path part (sometimes referred to as an absolute path or a relative URL).
According to the HTTP spec, redirects should specify a URI, which can be interpreted (by some people) to mean a fully qualified URL.4 Therefore, if you want to be pedantic about it, you probably should always use the _url
version when you use a named route as an argument to redirect_to
in your controller code.
4. http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
The redirect_to
method works perfectly with the relative URLs generated by _path
helpers, making arguments about the matter somewhat pointless. In fact, other than redirects, permalinks, and a handful of other edge cases, it’s the Rails way to use _path
instead of _url
. It produces a shorter string and the user agent (browser or otherwise) should be able to infer the fully qualified URL whenever it needs to do so, based on the HTTP headers of the request, a base element in the document, or the URL of the request.
As you read this book and as you examine other code and other examples, the main thing to remember is that help_url
and help_path
are basically doing the same thing. I tend to use the _url
style in general discussions about named route techniques but the _path
style in examples that occur inside view templates (e.g., with link_to
and form_for
). It’s mostly a writing style thing, based on the theory that the URL version is more general and the path version more specialized. In any case, it’s good to get used to seeing both and getting your brain to view them as very closely connected.
Using Literal URLs
You can, if you wish, hard-code your paths and URLs as string arguments to link_to
, redirect_to
, and friends. For example, instead of
link_to "Help", controller: "main", action: "help"
link_to "Help", "/main/help"
However, using a literal path or URL bypasses the routing system. If you write literal URLs, you’re on your own to maintain them. And in that case, I hope you’re an expert with find and replace! You can of course use Ruby’s string interpolation techniques to insert values, if that’s appropriate for what you’re doing, but really stop and think about whether you are reinventing Rails functionality if you go down that path.
As we’ll learn in Chapter 3, “REST, Resources, and Rails,” the best way to figure out what names you should use for your routes is to follow REST conventions, which are baked into Rails and simplify things greatly. Otherwise, you’ll need to think top-down; that is, think about what you want to write in your application code and then create the routes that will make it possible.
Take, for example, this call to link_to
:
link_to "Auction of #{item.name}",
controller: "items",
action: "show",
id: item.id
The routing rule to match that path is (a generic route)
get "item/:id" => "items#show"
It sure would be nice to shorten that link_to
code. After all, the routing rule already specifies the controller and action. This is a good candidate for a named route for items:
get "item/:id" => "items#show", as: "item"
Let’s improve the situation by introducing item_path
in the call to link_to
:
link_to "Auction of #{item.name} ", item_path(id: item.id)
Giving the route a name is a shortcut; it takes us straight to that route, without a long search and without having to provide a thick description of the route’s hard-coded parameters.
In fact, we can make the argument to item_path
even shorter. If you need to supply an id number as an argument to a named route, you can just supply the number, without spelling out the :id
key:
link_to "Auction of #{item.name}", item_path(item.id)
And the syntactic sugar goes even further: You can and should provide objects and Rails will grab the id automatically.
link_to "Auction of #{item.name}", item_path(item)
This principle extends to other segment keys in the pattern string of the named route. For example, if you’ve got a route like
get "auction/:auction_id/item/:id" => "items#show", as: "item"
you’d be able to call it
link_to "Auction of #{item.name}", item_path(auction, item)
and you’d get something like this as your path (depending on the exact id numbers):
/auction/5/item/11
Here, we’re letting Rails infer the ids of both an auction object and an item object, which it does by calling to_param
on whatever nonhash arguments you pass into named route helpers. As long as you provide the arguments in the order in which their ids occur in the route’s pattern string, the correct values will be dropped into place in the generated path.
Furthermore, it doesn’t have to be the id value that the route generator inserts into the URL. As alluded to a moment ago, you can override that value by defining a to_param
method in your model.
Let’s say you want the description of an item to appear in the URL for the auction on that item. In the item.rb
model file, you would override to_params
; here, we’ll override it so that it provides a “munged” (stripped of punctuation and joined with hyphens) version of the description, courtesy of the parameterize
method added to strings in Active Support.
1 def to_param
2 description.parameterize
3 end
Subsequently, the method call item_path(auction, item)
will produce something like
/auction/3/item/cello-bow
Of course, if you’re putting things like “cello-bow” in a path field called :id
, you will need to make provisions to dig the object out again. Blog applications that use this technique to create slugs for use in permanent links often have a separate database column to store the munged version of the title that serves as part of the path. That way, it’s possible to do something like
Item.where(munged_description: params[:id]).first!
to unearth the right item. (And yes, you can call it something other than :id
in the route to make it clearer!)
Courtenay Says ...
Why shouldn’t you use numeric ids in your URLs? First, your competitors can see just how many auctions you create. Numeric consecutive ids also allow people to write automated spiders to steal your content. It’s a window into your database. And finally, words in URLs just look better. (Google “German tank problem” to learn about how serial numbers on German tanks helped the allies win World War II.)
Rails gives you a variety of ways to bundle together related routing rules concisely. They’re all based on usage of the scope
method and its various shortcuts. For instance, let’s say that you want to define the following routes for auctions:
1 get 'auctions/new' => 'auctions#new'
2 get 'auctions/edit/:id' => 'auctions#edit'
3 post 'auctions/pause/:id' => 'auctions#pause'
You could DRY up your routes.rb
file by using the scope
method instead:
1 scope controller: :auctions do
2 get 'auctions/new' => :new
3 get 'auctions/edit/:id' => :edit
4 post 'auctions/pause/:id' => :pause
5 end
Then you would DRY it up again by adding the :path
argument to scope
:
1 scope path: '/auctions', controller: :auctions do
2 get 'new' => :new
3 get 'edit/:id' => :edit
4 post 'pause/:id' => :pause
5 end
The scope method accepts a :controller
option (or it can interpret a symbol as its first argument to assume a controller). Therefore, the following two scope definitions are identical:
scope controller: :auctions do
scope :auctions do
To make what’s going on more obvious, you can use the controller
method instead of scope
, in what’s essentially syntactic sugar:
controller :auctions do
The scope method accepts a :path
option (or it can interpret a string as its first parameter to mean a path prefix). Therefore, the following two scope definitions are identical:
scope path: '/auctions' do
scope '/auctions' do
New to Rails 4 is the ability to pass the :path
option symbols instead of strings. The scope definition
scope :auctions, :archived do
will scope all routes nested under it to the “/auctions/archived” path.
The scope method also accepts a :as
option that affects the way that named route URL helper methods are generated. The route
1 scope :auctions, as: 'admin' do
2 get 'new' => :new, as: 'new_auction'
3 end
will generate a named route URL helper method called admin_new_auction_url
.
URLs can be grouped by using the namespace
method, which is syntactic sugar that rolls up module, name prefix, and path prefix settings into one declaration. The implementation of the namespace
method converts its first argument into a string, which is why in some example code you’ll see it take a symbol.
1 namespace :auctions, :controller => :auctions do
2 get 'new' => :new
3 get 'edit/:id' => :edit
4 post 'pause/:id' => :pause
5 end
If you find yourself repeating similar segment key constraints in related routes, you can bundle them together using the :constraints
option of the scope
method:
1 scope controller: :auctions, constraints: {:id => /d+/} do
2 get 'edit/:id' => :edit
3 post 'pause/:id' => :pause
4 end
It’s likely that only a subset of rules in a given scope need constraints applied to them. In fact, routing will break if you apply a constraint to a rule that doesn’t take the segment keys specified. Since you’re nesting, you probably want to use the constraints
method, which is just more syntactic sugar to tighten up the rule definitions.
1 scope path: '/auctions', controller: :auctions do
2 get 'new' => :new
3 constraints id: /d+/ do
4 get 'edit/:id' => :edit
5 post 'pause/:id' => :pause
6 end
7 end
To enable modular reuse, you may supply the constraints
method with an object that has a matches?
method.
1 class DateFormatConstraint
2 def self.matches?(request)
3 request.params[:date] =~ /Ad{4}-dd-ddz/ # YYYY-MM-DD
4 end
5 end
6
7 # in routes.rb
8 constraints(DateFormatConstraint) do
9 get 'since/:date' => :since
10 end
In this particular example (DateFormatConstraint
), if an errant or malicious user input a badly formatted date parameter via the URL, Rails will respond with a 404 status instead of causing an exception to be raised.
A handy route listing utility is included in all Rails projects as a standard rake task. Invoke it by typing rake routes
in your application directory. For example, here is the output for a routes file containing just a single resources :products
rule:
$ rake routes
products GET /products(.:format) products#index
POST /products(.:format) products#create
new_product GET /products/new(.:format) products#new
edit_product GET /products/:id/edit(.:format) products#edit
product GET /products/:id(.:format) products#show
PATCH /products/:id(.:format) products#update
PUT /products/:id(.:format) products#update
DELETE /products/:id(.:format) products#destroy
The output is a table with four columns. The first two columns are optional and contain the name of the route and HTTP method constraint, if they are provided. The third column contains the URL mapping string. Finally, the fourth column indicates the controller and action method that the route maps to plus constraints that have been defined on that routes segment keys (if any).
Note that the routes task checks for an optional CONTROLLER
environment variable
$ rake routes CONTROLLER=products
would only lists the routes related to ProductsController
.
Juanito Says ...
While you have a server up and running on development environment, you could visit /rails/info/routes
to get a complete list of routes of your Rails application.
The first half of the chapter helped you to fully understand the generic routing rules of Rails and how the routing system has two purposes:
• Recognizing incoming requests and mapping them to a corresponding controller action, along with any additional variable receptors
• Recognizing URL parameters in methods such as link_to
and matching them up to a corresponding route so that proper HTML links can be generated
We built on our knowledge of generic routing by covering some advanced techniques such as using regular expressions and globbing in our route definitions, plus the bundling of related routes under shared scope options.
Finally, before moving on, you should make sure that you understand how named routes work and why they make your life easier as a developer by allowing you to write more concise view code. As you’ll see in the next chapter, once we start defining batches of related named routes, we’re on the cusp of delving into REST.
3.149.234.188