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.
- Alphabetically ordered routes
- Resource and resources
only
, notexcept
- Namespaces
- Constraints
- 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
andnamespaces
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 login
, logout
, 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 specifyingonly
with all routes listed, I omit theonly
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.