Tips for organizing your routes in Ruby on Rails

The routes.rb file is a crucial part of any Ruby on Rails development project. It’s essentially the map of our application, and keeping it organized is key. We usually start off with everything neat and tidy, organizing routes by modules or alphabetically, but over time, it often turns into a jungle that’s hard to navigate.

It’s like that storage room in your house where you keep things you don’t know where else to put. Initially, you know what’s in there, but one day you go in to find something and it’s become a battlefield, with an old skateboard on top of a moldy wooden box from your grandmother, and a picture of you as a baby next to Tony’s first collar, your first pet.

Since I’m not a fan of that kind of chaos, in this article I’ll share some practices we’ve developed over the years that have been very useful for organizing that storage room called routes.rb.

Here’s the list of tips organized by the impact they’ve had on my projects. Each can be applied independently, so feel free to jump straight to the one that interests you most if you don’t want to read them all.

  1. Alphabetically ordered routes
  2. Resource and resources
  3. only, not except
  4. Namespaces
  5. Constraints
  6. Concerns

1. Alphabetically ordered routes

This tip isn’t the first by chance; I consider it the most important of all. It’s brought us the best results when organizing routes, not just because of the alphabetical ordering, but due to the importance of establishing team agreements.

Personally, I believe one of the most important premises of Ruby on Rails is Convention over Configuration. Following the conventions proposed by the framework usually yields good results because it ensures that any developer on the team knows where to find a piece of code or where to add new instructions without hesitation. This is crucial — not just because it saves time, but also because it’s one less decision to make. After all, Steve Jobs wore the same outfit every day for a reason.

We should do the same with our routes: establish team rules to make maintaining them as simple and tidy as possible.

In different projects, we’ve followed various conventions: organizing by modules, splitting into different files, sorting alphabetically, among others. Without a doubt, the simplest and most practical approach has been to sort the routes file alphabetically. If you have a lot of routes, it might be helpful to separate them into different files, but I’d try to avoid that unless it’s absolutely necessary.

By alphabetical order, I mean sorting ALL routes alphabetically. This includes resources and namespaces at the general level and within each nesting.

2. Resource and resources

Just as the business layer in any project is represented by objects that communicate and interact with each other, I like to see routes as actions on resources. For this reason, almost all the routes we define in our projects are usually associated with a specific resource.

Rails provides different mechanisms to handle this, though the basics would be using resources and resource, with all their variations. Additionally, if needed, resource routes can be nested, resulting in something like the following:

resources :articles do
resources :comments
end

⚠️ Nesting can be very useful, but we need to be careful because it can introduce some complexity. I definitely recommend not using more than one level of nesting.

Shallow nesting

One problem with nesting is that the member routes of the nested resource end up inside the parent.

In the previous example, the routes for comments would be nested within an article:

GET /articles/:article_id/comments
GET /articles/:article_id/comments/new
POST /articles/:article_id/comments

GET /articles/:article_id/comments/:id <===== ⚠️
GET /articles/:article_id/comments/:id/edit <===== ⚠️
DELETE /articles/:article_id/comments/:id <===== ⚠️
PUT/PATCH /articles/:article_id/comments/:id <===== ⚠️

The first three routes look fine, but the routes that involve a specific comment seem odd within an article.

To address this, we could define the routes like this:

resources :articles do
resources :comments, only: [:index, :new, :create]
end
resources :comments, only: [:show, :edit, :update, :destroy]

This would generate the following routes, which in my opinion, make much more sense:

GET /articles/:article_id/comments
GET /articles/:article_id/comments/new
POST /articles/:article_id/comments

GET /comments/:id <===== ✅
GET /comments/:id/edit <===== ✅
DELETE /comments/:id <===== ✅
PUT/PATCH /comments/:id <===== ✅

The same result could be achieved using the shallow parameter:

resources :articles do
resources :comments, shallow: true
end

What should we do with routes that aren’t associated with a resource?

I see this as a potential bad smell. It’s quite likely that behind such a route, there’s a hidden resource or resources.

However, there are still some standalone routes in our routes.rb. Examples that come to mind are loginlogout, and perhaps a healthcheck.

post 'login' => 'sessions#login', as: :login
delete 'logout' => 'sessions#logout', as: :logout
get 'up' => 'rails/health#show', as: :rails_health_check

3. Only, not except

As a developer, I follow the rule “it’s always better to be explicit than implicit”. Many times, in our attempt to save a couple of words, a line of code, or even a comment (yes, I’m pro-comments), we end up creating issues in our application or making life difficult for a future developer who inherits our work.

I apply the same rule to routes, and I prefer only over except. While using except can be convenient, it could lead to generating routes that are not eventually used.

That’s why the first thing I do when generating a resource in my routes is to add the only parameter.

resources :products, only: %i[index new create show]
resources :users, only: %i[index new create destroy]

In the only case where I don’t include the only parameter—and breaking my own rule—is when the resource contains all the routes. In other words, instead of specifying only with all routes listed, I omit the only parameter altogether.

4. Namespaces

Namespaces allow us to separate our routes into groups of controllers, which is very useful for organizing logic into modules.

They are particularly valuable in the following situations:

1) When there are clearly separated subsystems. For example, a backoffice from a frontend.

2) When the application grows and we start to generate too much complexity, and we want to separate controllers into groups/modules.

namespace :admin do
resources :payments
resources :users
end

namespace :ads do
resource :report, only: %i[show]
end

namespace :finance do
resource :report, only: %i[show]
end

In this way, we would have the following controllers:

Admin::PaymentsController # app/controllers/admin/payments_controller.rb
Admin::UsersController # app/controllers/admin/users_controller.rb
Ads::ReportController # app/controllers/ads/report_controller.rb
Finance::ReportController # app/controllers/finance/report_controller.rb

5. Constraints

Constraints are restrictions that can be assigned to routes. When defining a constraint, the indicated method is invoked on the request object, and the returned value is compared with the parameterized value.

A classic example is subdomain constraint. For instance, we could define a constraint for routes that are for admin.

namespace :admin do
constraints subdomain: 'admin' do
resources :users
end
end

6. Concerns

Another useful tool is concerns. These allow us to define common routes that can then be used for different resources.

To give an example, I’ll share a concern we used in a recent project. The application had various resources, and these resources had the ability to be reordered within a list, i.e., change their position. Since the route for changing this position was repeated throughout the codebase, we extracted it into a concern:

concern :positionable do
patch :update_position, on: :member
end

resources :categories, only: %i[index new create], concerns: %i[positionable]
resources :category_groups, concerns: %i[positionable]
resources :sections, only: %i[index new create destroy], concerns: %i[positionable]

⚠️ Concerns introduce an indirection in the code. It’s important to ask whether it’s worth extracting common routes. Sometimes, it’s preferable to repeat code than to introduce indirection.

These are some of the practices we follow to build our routes files. It doesn’t mean you have to follow the same rules with your team; these are what worked for us.

The most important thing is to agree on conventions with your team so that the routes file is as tidy and maintainable as possible.