Rails 5 Routing Cookbook: 10 recipes for the novice Rails developer and beyond

Rui Freitas
Jan 26, 2019 · 9 min read

In this article, I examine the rails routing system, including its basic syntax, standard CRUD routes, nested resources, member and collection routes, namespaced and scoped routes, and other advanced patterns.

I often see beginners not taking full advantage of the powerful routing engine provided by Ruby on Rails. Often they’re using more cumbersome syntax duplicating code or, worse, breaking Rails conventions left and right. But before we dive into concrete examples, it’s important to understand what the Rails routes actually are. According to Obie Fernandez in The Rails 5 Way:

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

In other words, the routing system maps URLs to controller Actions using rules in ruby code, but with a special syntax — an internal domain-specific language (DSL). It’s like a switchboard, establishing connections in our application from one request to the controller. These rules can then generate patterns automatically based on conventions, such as REST (Representational State Transfer) resources.

1. Basic Syntax

All routes are coded in config/routes.rb

Generic routes in Ruby on Rails follow the following convention:

HTTP_VERB PATH, to: ‘CONTROLLER#ACTION’

The HTTP_VERB is one of the request methods that indicate the desired action to be performed for a given resource. The standard verbs used in Rails are GET, POST, PUT/PATCH, and DELETE. You can read more about HTTP methods here.

The PATH is the URL path inside the root path of our Ruby on Rails application, which is mapped to a specific CONTROLLER to execute a specific ACTION (a ruby method defined in that controller).

Let’s say that I have a RubyGarage application that stores a list of cars. In order to display the list of cars, I could write the following route:

get 'cars', to: 'cars#index'

Here, I am saying that when I visit the URL http://my-ruby-garage.com/cars in the browser, which translates into a GET request, the index action of the Cars controller is executed or called.

Routes can also have dynamic elements. Let’s imagine that in the same application, I want to list cars by brand or make. In that case, I could have the following route:

get 'cars/:brand', to: 'cars#index'

This route behaves very much like the one before but will capture the part that follows the slash after cars in the URL, like inhttp://my-ruby-garage.com/cars/toyota, where toyota would be set in the params hash with the key :brand and available for use in the cars#index action.

In the above example, the route is made up of the following elements:

  • Static string: cars
  • Slash: /
  • Segment key: :brand
  • Controller action mapping: 'cars#index'
  • HTTP verb constraining method: get

2. Standard CRUD

CRUD stands for create, read, update and delete. In Rails, there are seven standard CRUD actions: index, show, new, create, edit, update, and destroy, which relate to specific HTTP verbs and are usually implemented using specific ActiveRecord methods.

Image for post
Image for post

The routes for these actions can be implemented like this:

get 'cars', to: 'cars#index'get 'cars/new', to: 'cars#new'
post 'cars', to: 'cars#create'
get 'cars/:id', to: 'cars#show'get 'cars/:id/edit', to: 'cars#edit'
patch 'cars/:id', to: 'cars#update'
delete 'cars/:id', to: 'cars#destroy'

Notice that the show route must come after the new route or otherwise the string new that is part of the latter would be captured by the segment key :id of the former.

This means that in Rails routes, order matters! Route matching in Rails is non-gready or thrifty and matches from top to bottom until it finds a result. As Obie Fernandez explains:

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

Besides the concern with order, the above syntax is also very verbose, which means there is a lot of room for typos and mistakes to occur. That’s why the preferred way to define all 7 CRUD routes is the following:

resources :cars

Neat, right?

Not only does the above syntax create all seven routes as defined manually before, but it also conveniently names each route with a prefix. As such, resources is equivalent to:

get 'cars', to: 'cars#index'
post 'cars', to: 'cars#create'
get 'cars/new', to: 'cars#new', as: :new_car
get 'cars/:id/edit', to: 'cars#edit', as: :edit_car
get 'cars/:id', to: 'cars#show', as: :car
put 'cars/:id', to: 'cars#update'
patch 'cars/:id', to: 'cars#update'
delete 'cars/:id', to: 'cars#destroy'

The create action is under the same prefix cars and is differentiated from the index action by its HTTP verb. Same goes for the show, update and destroy actions, which fall under the car prefix.

You should bear in mind that Rails is an opinionated framework and one of its underlying principles is convention over configuration. This means that unless you have a very good reason, you should not break any of the above patterns and naming conventions.

For instance, if I decided to code the index action as get 'cars/index' or the create action as post 'car/create', this would break the out-of-the-box Rails behaviour for helper methods such as form_for, link_to, and redirect_to.

The consequence of breaking conventions without intent is chaos and it produces a tangled mess which will have you fighting against the framework for hours on end.

Image for post
Image for post
Photo by Daniele Levis Pelusi on Unsplash

3. Singular Resource Routes

It’s also possible to have routes for singular resources, that is, resources that can be looked up without specifying an :id, such as the ones applying to a current_user or current_account. This can be achieved by using Rails’ built-in singular resources.

resource :profile 

4. Nested Routes

Sometimes we need to nest resources inside other resources. For example, if I want to create a booking for a specific car in my garage, it might be more convenient to grab the :id for that car from the URL rather than a form hidden field, which could be tampered with by a malicious user. The way we create nested resources in rails is as follows:

resources :cars do 
resources :bookings
end

This creates all seven CRUD actions for bookings nested in cars. Usually, though, you don’t need all seven routes. And even when you do, certainly not all of them need to be nested. Let’s say that what we need is the action to create a booking (we’ll assume the form to create a booking lives in the show action of the car) and to edit and update an existing booking. Do I need to know the :idof a car to edit/update a booking? The answer is obviously no. Therefore, I want my application to respond to the following URLs:

# to CREATE a booking
POST request to http://my-ruby-garage.com/cars/:id/bookings/create
# to EDIT a booking
GET request to http://my-ruby-garage.com/bookings/:id/edit
# to UPDATE a booking
PATCH request to http://my-ruby-garage.com/bookings/:id/

Which are generating by the following code:

resources :cars do 
resources :bookings, only: [:create]
end
resources :bookings, only: [:edit, :update]

Rails lets us modify which of the standard CRUD routes should be generated by supplying an array of symbols or a single symbol to the option :only or its opposite :except.

5. Non-CRUD Routes

We are not limited to the seven CRUD resource routes. We can also specify routes that apply to a single resource (member routes) or to several resources (collection routes).

Member routes

Continuing with our RubyGarage example, cars can be parked in or removed from the garage. Let’s imagine that we have controller actions that are able to perform these actions that modify some attribute of a specific car.

resources :cars do 
member do
patch :park
patch :remove
end
end

The above allows us to send patch requests to http://my-ruby-garage.com/cars/:id/park and http://my-ruby-garage.com/cars/:id/remove and find the specific car in the controller before modifying the resource accordingly.

Collection routes

In the same way, sometimes we want to perform some action on several resources at the same time. For example, maybe we need to park and remove a collection of cars at once. We can use the following code:

resources :cars do 
collection do
post :park, as: :bulk_park
post :remove, as: :bulk_remove
end
end

Here, we set up our application to respond to http://my-ruby-garage.com/cars/park and http://my-ruby-garage.com/cars/remove and, respectively, name these actions bulk_park and bulk_remove. Remember that we can used named paths to generate the URL paths inside our application. To build a path link to park a collection of cars, we could use:

<%= link_to "Park Cars", bulk_park_path, method: :post, class: "button" %>

6. Namespaced Routes

Namespaced routes prefix the URL path of the resources inside the namespace block and will try to locate the relevant controllers under a module with the same name as the namespace. Typical uses for this pattern are admin namespaces and api namespaces.

namespace :factory do 
resources :cars
end

This example builds the following routes:

Prefix       Verb   URI Pattern                Controller#Action factory_cars GET    /factory/cars(.:format)     factory/cars#index
POST /factory/cars(.:format) factory/cars#create factory_car GET /factory/cars/:id(.:format) factory/cars#show
PATCH /factory/cars/:id(.:format) factory/cars#update
PUT /factory/cars/:id(.:format) factory/cars#update
DELETE /factory/cars/:id(.:format) factory/cars#destroy

And the controller would have to be namespaced too :

class Factory::CarsController < ApplicationController
# ...
end

7. Scoped Routes

The scope method allows us to remain DRY and bundle together related routing rules. When used without options, it is similar to namespace, but the relevant controllers don’t have to be namespaced with a module.

scope :factory do 
resources :cars
end

Generates the following routes:

Prefix       Verb   URI Pattern                Controller#Action factory_cars GET    /factory/cars(.:format)     cars#index
POST /factory/cars(.:format) cars#create factory_car GET /factory/cars/:id(.:format) cars#show
PATCH /factory/cars/:id(.:format) cars#update
PUT /factory/cars/:id(.:format) cars#update
DELETE /factory/cars/:id(.:format) cars#destroy

Scope supports three options: module, path and as.

Let’s say we have a multi-step wizard to create a new car in the factory which is handled by a controller living in a wizards module. We want the path to appear in the browser as http://my-ruby-garage.com/create-a-car and to be able to reference this route inside our application as create_car. There is a module Wizard::Car that knows each step of the wizard.

scope module: 'wizards', path: 'create-a-car', as: 'create_car' do
Wizard::Car::STEPS.each do |step|
get step, to: "cars##{step}"
end
post :validate-step, to: 'cars#validate_step'
end

The above code creates the same pattern for each step. For instance, step1 is accessible in the browser via the URL http://my-ruby-garage.com/create-a-car/step1, its corresponding form submits a post request to http://my-ruby-garage.com/create-a-car/validate-step, and the path can the summoned by calling create_car_step1_path.

8. Route Redirects

Rails also let us do redirects directly in routes.rb. In the preceding example, maybe I want anyone landing on http://my-ruby-garage.com/create-a-car to be automatically redirected to the first step. This can be accomplished with the following code:

get 'create-a-car', 
to: redirect("/create-a-car/#{Wizard::SpotAccount::STEPS.first}")

9. Route Defaults

You can define default parameters in a route by passing a hash for the :defaults option.

resources :cars do
collection do
get :export, defaults: { format: 'csv' }
end
end

Using this, visiting http://my-ruby-garage.com/cars/export will call the export action in the cars controller and the corresponding controller action will respond with a csv by default.

10. Route Globbing

Using wildcard segments (fragments prefixed with a star), we can specify that the remaining parts of a route should be matched to a particular parameter. This is called route globbing.

get '/rent/*slugs', to: 'cars#index', as: :rent_cars

This route would match http://my-ruby-garage.com/rent/lisbon/suv-sedan and set params[:slugs] to “lisbon/suv-sedan”. This could then be used in a lookup or filtering system against our database to find cars in Lisbon of the type suv or sedan.

I am writing a gem Slugcatcher with this functionality in mind in order to tag Rails models that can be looked up as route slugs.

Light the Fuse and Run

Web development in Ruby on Rails, React, Vue.js and Elixir

Rui Freitas

Written by

Lead Teacher @ Le Wagon | Web Developer @ Light the Fuse and Run: http://lightthefuse.run/ | Photographer @ Rod Loboz: https://blog.rodloboz.com/

Light the Fuse and Run

Web development in Ruby on Rails, React, Vue.js and Elixir

Rui Freitas

Written by

Lead Teacher @ Le Wagon | Web Developer @ Light the Fuse and Run: http://lightthefuse.run/ | Photographer @ Rod Loboz: https://blog.rodloboz.com/

Light the Fuse and Run

Web development in Ruby on Rails, React, Vue.js and Elixir

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store