6.2. HTTP GET Requests for Singular Resources

As you might know the HTTP protocol uses different so called verbs to access content on the webserver (e.g. GET to request a page or POST to send a form to the server). First we'll have a look at GET requests.
Create a controller with three pages:
$ rails generate controller Home index ping pong
      create  app/controllers/home_controller.rb
       route  get "home/pong"
       route  get "home/ping"
       route  get "home/index"
       [...]
$
Now rake routes lists a route for these three pages:
$ rake routes
    Prefix Verb URI Pattern           Controller#Action
home_index GET /home/index(.:format) home#index
 home_ping GET /home/ping(.:format)  home#ping
 home_pong GET /home/pong(.:format)  home#pong
$
The pages can be accessed at the following URLs after starting the Rails server with rails server:
http://0.0.0.0:300/home/ping
With the output home#index, Rails tells us that the route /home/index goes into the controller home and there to the action/method index. These routes are defined in the file config/routes.rb. rails generate controller Home index ping pong has automatically inserted the following lines there:
  get "home/index"
  
  get "home/ping"
  
  get "home/pong"

Naming a Route

A route should also always have an internal name which doesn't change. In Section 6.2, “HTTP GET Requests for Singular Resources” there is the following route:
home_pong GET /home/pong(.:format)  home#pong
This route has the automatically created name home_pong. Generally, you should always try to work with the name of the route within a Rails application. So you would point a link_to to home_pong and not to /home/pong. This has the big advantage that you can later edit (in the best case, optimize) the routing for visitors externally and do not need to make any changes internally in the application. Of course, you need to enter the old names with :as in that case.

as

If you want to define the name of a route yourself, you can do so with :as. For example, the line
get "home/pong", as: 'different_name'
results in the route
different_name GET    /home/pong(.:format)      home#pong

to

With to you can define an other destination for a rout. For example, the line
get "home/applepie", to: "home#ping"
results in the route
home_applepie GET /home/applepie(.:format) home#ping

Parameters

The routing engine can not just assign fixed routes but also pass parameters which are part of the URL. A typical example would be date specifications (e.g. http://example.com/2010/12/ for all December postings).
To demonstrate this, let's create a mini blog application:
$ rails new blog
  [...]
$ cd blog
$ rails generate scaffold post subject content published_at:date
  [...]
$ rake db:migrate
  [...]
$
As example data in the db/seeds.rb we take:
Post.create(subject: 'A test', published_at: '01.10.2011')
Post.create(subject: 'Another test', published_at: '01.10.2011')
Post.create(subject: 'And yet one more test', published_at: '02.10.2011')
Post.create(subject: 'Last test', published_at: '01.11.2011')
Post.create(subject: 'Very final test', published_at: '01.11.2012')
With rake db:seed we populate the database with this data:
$ rake db:seed
$
If we now start the Rails server with rails server and go to the page http://0.0.0.0:3000/posts in the browser, we will see this:
Index view of all posts
For this kind of blog it would of course be very useful if you could render all entries for the year 2010 with the URL http://0.0.0.0:3000/2010/ and all entries for October 1st 2010 with http://0.0.0.0:3000/2010/10/01. We can do this by using optional parameters. Please enter the following configuration in the config/routes.rb:
Blog::Application.routes.draw do
  resources :posts

  get ':year(/:month(/:day))', to: 'posts#index'
end
The round brackets represent optional parameters. In this case, you have to specify the year, but not necessarily the month or day. rake routes shows the new route at the last line:
$ rake routes
   Prefix Verb   URI Pattern                      Controller#Action
    posts GET    /posts(.:format)                 posts#index
          POST   /posts(.:format)                 posts#create
 new_post GET    /posts/new(.:format)             posts#new
edit_post GET    /posts/:id/edit(.:format)        posts#edit
     post GET    /posts/:id(.:format)             posts#show
          PATCH  /posts/:id(.:format)             posts#update
          PUT    /posts/:id(.:format)             posts#update
          DELETE /posts/:id(.:format)             posts#destroy
          GET    /:year(/:month(/:day))(.:format) posts#index
$ 
If we do not change anything else, we still get the same result when calling http://0.0.0.0:3000/2011/ and http://0.0.0.0:3000/2011/10/01 as we did with http://0.0.0.0:3000/posts. But have a look at the output of rails server for the request http://0.0.0.0:3000/2011
Started GET "/2011" for 127.0.0.1 at 2013-07-17 11:08:58 +0200
  ActiveRecord::SchemaMigration Load (0.1ms)  SELECT "schema_migrations".* FROM "schema_migrations"
Processing by PostsController#index as HTML
  Parameters: {"year"=>"2011"}
  Post Load (0.1ms)  SELECT "posts".* FROM "posts"
  Rendered posts/index.html.erb within layouts/application (21.3ms)
Completed 200 OK in 96ms (Views: 76.3ms | ActiveRecord: 0.5ms)
The route has been recognised and an "year" => "2011" has been assigned to the hash params (written misleadingly as Parameters in the output). Going to the URL http://0.0.0.0:3000/2010/12/24 results in the following output, as expected:
Started GET "/2010/12/24" for 127.0.0.1 at 2013-07-17 11:11:21 +0200
Processing by PostsController#index as HTML
  Parameters: {"year"=>"2010", "month"=>"12", "day"=>"24"}
  Post Load (0.2ms)  SELECT "posts".* FROM "posts"
  Rendered posts/index.html.erb within layouts/application (3.4ms)
Completed 200 OK in 9ms (Views: 8.1ms | ActiveRecord: 0.2ms)
In case of the URL http://0.0.0.0:3000/2010/12/24, the following values have been saved in the hash params: "year"=>"2010", "month"=>"12", "day"=>"24".
In the controller, we can access params[] to access the values defined in the URL. We simply need to adapt the index method in app/controllers/posts_controller.rb to output the posts entered for the corresponding date, month or year:
# GET /posts
# GET /posts.json
def index
  # Check if the URL requests a date.
  if Date.valid_date? params[:year].to_i, params[:month].to_i, params[:day].to_i 
    start_date = Date.parse("#{params[:day]}.#{params[:month]}.#{params[:year]}")
    end_date = start_date

  # Check if the URL requests a month
  elsif Date.valid_date? params[:year].to_i, params[:month].to_i, 1 
    start_date = Date.parse("1.#{params[:month]}.#{params[:year]}")
    end_date = start_date.end_of_month
      
  # Check if the URL requests a year
  elsif params[:year] && Date.valid_date?(params[:year].to_i, 1, 1)
    start_date = Date.parse("1.1.#{params[:year]}")
    end_date = start_date.end_of_year
  end

  if start_date && end_date
    @posts = Post.where(published_at: start_date..end_date)
  else
    @posts = Post.all
  end
end
If we now go to http://0.0.0.0:3000/2011/10/01 , we can see all posts of October 1st 2011.
Index view for 1st Oct 2011

Constraints

In the section called “Parameters” I showed you how you can read out parameters from the URL and pass them to the controller. Unfortunately, the entry defined there in the config/routes.rb
get ':year(/:month(/:day))', to: 'posts#index'
has one important disadvantage: it does not verify the individual elements. For example, the URL http://0.0.0.0:3000/just/an/example will be matched just the same and then of course results in an error:
Error message
In the log output in log/development.log we can see the following entry:
Started GET "/just/an/example" for 127.0.0.1 at 2013-07-17 12:04:01 +0200
Processing by PostsController#index as HTML
  Parameters: {"year"=>"just", "month"=>"an", "day"=>"example"}
Completed 500 Internal Server Error in 2ms

ArgumentError (invalid date):
  app/controllers/posts_controller.rb:19:in `parse'
  app/controllers/posts_controller.rb:19:in `index'
Obviously, Date.parse( "example.an.just") cannot work. A date is made up of numbers, not letters.
Constraints can define the content of the URL more precisely via regular expressions. In the case of our blog, the config/routes.rb with contraints would look like this:
Blog::Application.routes.draw do
  resources :posts

  get ':year(/:month(/:day))', to: 'posts#index', constraints: { year: /\d{4}/, month: /\d{2}/, day: /\d{2}/ }
end

Buy the new Rails 5.1 version of this book.

Please note that you cannot use regex anchors such as "^" in regular expressions in a constraint.
If we go to the URL again with this configuration, Rails gives us an error message "No route matches":
Routing Error

Redirects

Our current application answers request in the format YYYY/MM/DD (4 digits for the year, 2 digits for the month and 2 digits for the day). That is ok for machines but maybe a human would request a single digit month (like January) and a single digit day without adding the extra 0 to make it two digits. We can fix that with a couple of redirect rules which catch these URLs and redirect them to the correct ones.
Blog::Application.routes.draw do
  resources :posts

  get ':year/:month/:day', to: redirect("/%{year}/0%{month}/0%{day}"), constraints: { year: /\d{4}/, month: /\d{1}/, day: /\d{1}/ }
  get ':year/:month/:day', to: redirect("/%{year}/0%{month}/%{day}"), constraints: { year: /\d{4}/, month: /\d{1}/, day: /\d{2}/ }
  get ':year/:month/:day', to: redirect("/%{year}/%{month}/0%{day}"), constraints: { year: /\d{4}/, month: /\d{2}/, day: /\d{1}/ }
  get ':year/:month', to: redirect("/%{year}/0%{month}"), constraints: { year: /\d{4}/, month: /\d{1}/ }

  get ':year(/:month(/:day))', to: 'posts#index', constraints: { year: /\d{4}/, month: /\d{2}/, day: /\d{2}/ }
end
With this set of redirect rules, we can ensure that a user of the page can also enter single-digit days and months and still ends up in the right place, or is redirected to the correct format.

Buy the new Rails 5.1 version of this book.

Redirects in the config/routes.rb are by default http redirects with the code 301 ("Moved Permanently"). So even search engines will profit from this.

Thank you for your support and the visibility by linking to this website on Twitter and Facebook. That helps a lot!