Rails router 5 uncommon features you will fall in love with cover

There is no doubts that the router is a foundation of every Rails application. Usually, it is full of single routes and resources. However, as your application grows and becomes more complex it is very tough to maintain your routes.rb file clean and readable. Thus I would like to present you a few features of Rails Router that will help you to grasp the nettle.

1. Shallow resource nesting

A resource is probably the most favourite router structure if you follow REST standard. It allows you to pack a complete set of routes for a single resource into one line of code.

  resources :users

What's more, it is highly customizable. We may filter available routes, define your own controller to use or even add a prefix for aliases.

  resources :people, only: [:index, :new, :create], controller: 'users', as: 'regular_users'

It is equivalent to

  get '/people', to: 'users#index', as: :regular_users
  get '/people/new', to: 'users#new', as: :new_regular_user
  post 'people', to: 'users#create'

We may also nest resources to reflect the structure of our resources.

  resources :companies do
    resources :brands do
      resources :users
    end
  end

However, such a construction recognizes very long paths (/companies/1/brands/3/users/5) and its usage may become awkward. Therefore it is strongly advised not to nest resources more than 1 level deep (as Jamis Buck says).

Fortunately, the router brings us a shallow option that polishes the solution a little bit. As we know resource's methods can be divided into two categories. Collection actions (index, new, create) should be scoped under the parent to get a sense of a hierarchy, but all member actions can be defined out of the scope of a parent.

  resources :companies, only: [:index] do
    resources :users, only: [:index]
  end
  resources :users, only: [:show]

  company_users GET  /companies/:company_id/users(.:format) users#index
      companies GET  /companies(.:format)                   companies#index
           user GET  /users/:id(.:format)                   users#show

shallow option lets us simplify that to

  resources :companies, only: [:index] do
    resources :users, only: [:index, :show], shallow: true
  end

  company_users GET  /companies/:company_id/users(.:format) users#index
           user GET  /users/:id(.:format)                   users#show
      companies GET  /companies(.:format)                   companies#index

What's more, the router handles shallowing of all custom routes you define within a nested resource. We may also define shallow option in the parent resource so that every child will be shallow.

2. Concerns

Sometimes we want to use a single resource in many different contexts. For example, building a kind of a social network based on users' interactions we would like to let people express their feelings in a form of comment related to posts, photos, events, etc.

  resources :messages do
    resources :comments
    resources :ratings, only: [:index, :create, :update]
  end

  resources :photos do
    resources :comments
    resources :ratings, only: [:index, :create, :update]
  end

  resources :events do
    resources :comments
    resources :ratings, only: [:index, :create, :update]
  end

To avoid duplicating routes in such a case we can make use of a concern structure.

  concern :commentable do
    resources :comments
    resources :ratings, only: [:index, :create, :update]
  end

  resources :messages, concerns: :commentable
  resources :photos, concerns: :commentable
  resources :events, concerns: :commentable

We are not only limited to use concerns with resources. We can place them in any other place inside the routes.

  scope :posts do
    concerns :commentable
  end

3. Namespaces and Scopes

One of the rules of thumb we follow when starting to work on a project is to group functionalities under more common namespaces. It helps us to keep the code clean and maintainable as the application grows and becomes more complex. Rails router gives us two possibilities to deal with that: namespace and scope. They work slightly different so let's take a look at each of them.

Namespace

Every rails route definition consists of three parts:

  • URI pattern that is to be matched
  • Action that is to be taken when a pattern is matched
  • Alias that can be used as a shortcut for a given path

When we use a namespace, it will prefix all the three parts of a route.

  namespace :admin do
    resources :users, only: [:index, :create, :destroy]
  end

The above code would generate following routes:

 admin_users GET    /admin/users(.:format)                        admin/users#index
             POST   /admin/users(.:format)                        admin/users#create
  admin_user DELETE /admin/users/:id(.:format)                    admin/users#destroy

As we can see, admin prefix was added to the URI path(admin/users/), to the controller (admin/users#index) and to the alias admin_users. Keep in mind that with this route definition Rails will try to find Admin::UsersController in a module app/controllers/admin/users_controller.rb.

Scope

scope structure is more flexible and allows you to adjust your route to the context. By default scope will add a prefix only to the URI pattern and nor the controller neither the alias will be affected.

  scope :admin do
    resources :users, only: [:index, :create, :destroy]
  end

 users GET    /admin/users(.:format)                        users#index
       POST   /admin/users(.:format)                        users#create
  user DELETE /admin/users/:id(.:format)                    users#destroy

However, scope supports three options: module, path and as. Thanks to that we may adjust the route for our needs.

  scope module: 'admin', path: 'administrator', as: 'master' do
    resources :users, only: [:index, :create, :destroy]
  end

master_users GET    /administrator/users(.:format)     admin/users#index
             POST   /administrator/users(.:format)     admin/users#create
 master_user DELETE /administrator/users/:id(.:format) admin/users#destroy

4. Constraints

Rails router offers a number of options to enhance our routes using several kinds of constraints. They increase the security of our application by ensuring all incoming requests are understandable by our system and have all necessary data to be processed correctly. Let me mention few examples of routes constraints.

Segment Constraints

Adding the :constraint option we enforce a format for a dynamic segment.

  get 'users/:id', to: 'users#show', constraints: { id: /\d+/ }
  get 'users/:id', to: 'users#info', constraints: { id: /[A-Za-z]+/ }

Now we can bind a path to different actions based on whether a numerical ID or a slug is provided as a parameter.

Request-Based Constraints

It is possible to define constraints related to the Request Object. As official Rails Guide for Routing states we can constraint a route based on any method on the Request Object that returns a String. Therefore we can use a hostname, a domain, an ip address and much more to process the request. To give an example how it might improve our system imagine you develop a multilingual website. The functionality and content differ based on a subdomain. With request-based constraints, we can restrict particular resources to accept only specified requests.

  resources :photos

  constraints subdomain: 'es' do
    resources :photos, only: [:index]
  end

The above code will tell Rails to generate the complete set of photos routes accepting requests without subdomain whereas for es subdomain only index route will be available.

Advanced Constraints

If a more advanced constraint is needed we can define a custom class that responds to a matches? message and use it as a constraint for a single route or define a block that will constrain multiple routes at once. It may support IP whitelist or blacklist to direct unwanted requests to a specific endpoint of our application.

5. Translated Paths

Usually, we don't take care of how our URLs look like until we deploy our application to the production and our marketing specialist runs a campaign to make the website famous. If we assume our website is targeted for one country (defined by a specific TLD name) it makes sense to translate the latter part of our URLs. Thanks to that users browsing Google search results (based on localized keywords) will see more understandable addresses and will likely to click on our links.

Rails router supports path translation with a very simple method, including path_names helper.

  resources :companies, only: [:index, :new, :edit], path: 'firmy', path_names: { new: 'nowa', edit: 'edytuj' }

     companies GET  /firmy(.:format)            companies#index
   new_company GET  /firmy/nowa(.:format)       companies#new
  edit_company GET  /firmy/:id/edytuj(.:format) companies#edit

With the above code, all paths are translated whereas controller action names remain the same.

Summary

As you can see Rails router is much more than paths and resources. It gives a lot of tools to handle incoming requests properly and keep the routing structure consistent. If you are curious about other features I suggest reading the official guide. I also found Rails 5 Routes: Scope vs Namespace article very helpful when writing this post.

Post tags:

Join our awesome team
Check offers

Work
with us

Tell us about your idea
and we will find a way
to make it happen.

Get estimate