Ruby on Rails — Best Practices Every Developer Should Know

This article explains the best practices that one should follow while developing applications using Ruby on Rails with real-world examples!
These are the goals we would achieve by following this article and hopefully in the end attain the highest level of inner peace as a developer :)
- Code Reusability
- High Velocity
- Performance
- Maintainability
Contents
- Philosophies of Ruby on Rails
- Fat-Model-Skinny-Controller
- Module Utilization
- N+1 Query Problem
- Preloading Data
- Custom Controller Actions
- Parameter Validation
- Routes Conventions
- Must-have Gems
- Conclusion
Philosophies of Ruby on Rails
Before we get into the best practices of Ruby on Rails, we need to understand the philosophies it tries to implicate on its developers.
Convention over Configuration
This is a design paradigm that was kept in mind while Ruby on Rails was being developed. The development time of applications is drastically reduced due to this philosophy because Rails provides methods, functions, object-oriented principles, and much more out of the box which makes the lives of the developers using it much simpler.
DRY — Don’t Repeat Yourself
Ruby on Rails provides various features (some of which we’ll be looking at in this article) that can enable the developer not to repeat code and reuse existing code to reduce development time, increase readability, and maintain the application easily.
The following is the DB schema I will be referring to while explaining the best practices. It’s a very basic implementation of a website that allows users to write blogs and publish blogs under publications.

content
in the blogs table is a jsonb
format type that stores keywords
& body
of the Blog.
Let’s jump into the best practices to follow while working with Ruby on Rails for creating robust and scalable backend applications!
Fat-Model-Skinny-Controller
One of the main things we always want to keep in mind while creating new APIs is that the API should be as simple as possible, and the simplest API is where it does not deal with any business logic and only deals with Requests and Response. One way to achieve this is by outsourcing some of the business logic to the Models themselves, which are used for the business logic.
Let’s consider that we want to fetch the frequency of keywords present in the body of a blog.

This is what the controller would look like.

The entire logic of getting the frequency of keywords could have easily been written in the controller as well, there wouldn’t be any change in the API response. But if you think about it, this not only makes your controller more readable, but this function can be useful in other APIs as well, thus making it a potentially reusable piece of code!
If you think a piece of code may be reused later, it most likely will be the case in the future.
Module Utilization
Now, let’s say there are requirements that cannot be offloaded to Models, then Modules are your best friend in order to keep your controller clean.
One example could be that you want to fetch the blogs of a particular user, then the code for getting the user data and mapping it to a hash could be written in a module. Let’s call it BlogHelper
which can be created under the helpers
folder that Rails already provides.

This is what the controller would look like:

Notice the difference between a Module and a Model in our example DB schema?
When we are dealing with one entity, we should create a Model function and when we are dealing with N number of entities we should create a Module function.
This is not the only use of a Module function, it can also be used for creating other types of reusable functions. One example could be if you have to do time-related computations or implement any custom algorithm.
N+1 Query Problem
This is a very common Object-Relation Mapping problem, and overlooking this problem is easy since Rails abstracts the database queries away with the help of ActiveRecord & Models.
The problem arises when you run a database query to get the IDs of the Parent table and use those IDs to make N queries on the Child table one by one, making it the N+1 query problem.
The solution to this problem is to run a constant number of queries.
One example that I can think of while keeping our DB schema in mind is if we want to create an API that stores N Blogs in the database.
The naive way of doing this would be:

The first query is used to fetch the users who are the creators of the Blog using their emails. The next N queries are executed in the blogs.each
loop where the blogs are saved one at a time.
A better way of achieving the same result:

The first query is executed as it is. But in the loop, we only create the Blog objects and add them to an array. Once the loop is completed, we save all Blog objects together using one query. In total, this function required only 2 database queries!
Model.import!
is not a feature that is part of Rails out of the box, but we can do this because of an amazing Gem called activerecord-import!
Preloading Data
Preloading data means prefetching the data from the database in order to reduce the number of queries that the backend application makes.
Let’s say we want to fetch the Blogs of a User under a Publication.

The first query is made to fetch the Publication-Blog mappings of a User. Then, we loop over that array and find the Blog on line 7 using publication.blog
. This is possible since we have an ActiveRecord association created. Great right? Not really.
This is another case of the N+1 Query Problem.
In order to understand this better, let’s dive a little deeper.
In my local database, I have created 3 Publication-Blog mappings, and once I run the above code, then Rails makes 3 database queries for fetching the Blog one at a time.

In order to fix this issue, we don’t have to do much. We just need to change our initial query.

includes
is a way of preloading the data where you can provide the Model(s) that are associated to fetch and store it in RAM. This is the output on the Rails server logs:

Voila! After preloading, only one query was made with an array of IDs to fetch all Blogs from the database. This is the beauty of Ruby on Rails, just by making a simple change we are able to achieve the same result with better performance!
We can take this one step further. Notice the other two Database calls for User
& PublicationBlogMapping
models?

Eager Loading basically tries to fetch everything in a single database query.

As we can see, one query was made for the entire API. But we can also see how big and complex the database query is. This approach should not be used for every case, it is beneficial only in some cases, so use this with caution. Sometimes it’s better to run 3 small queries instead of one complex query. In our case, the includes
approach is better. We can take a call on whether to use includes
or eager_load
by looking at the query executed in the server logs.
Custom Controller Actions
If there is a case where we want to process data before or after
one or more APIs, we should consider using Custom Controller Actions.
Let’s consider the following 3 APIs for one of our example Models:
- Get Blog by ID
- Update Blog by ID
- Delete Blog by ID
If you notice, there is one thing in common. Blog ID.
An obvious thing we would want to do first is to validate whether the Blog even exists or not with the help of the ID.

Isn’t that amazing? Instead of adding those 4 lines in every controller, we can use this built-in Rails feature which executes a given function before the actual API.
This not only makes our code reusable but if you notice, it also implements a basic form of Access-Control. On line 6, the User is also added in the Database query which ensures the User can only get the Blogs they have written.
Parameter Validation
This is a step that requires some initial work while writing the APIs, but it is a good practice and can be useful in the following ways:
- Helps other developers not make mistakes and break the code during development.
- Prevents malicious users from sending corrupted data.
There is a gem that enables us to easily add Parameter Validations to our routes — rails-param!
Here is an example of how the parameter validation would look like if the API expected this data:
{
"name": "Blog 1",
"content": {
"keywords": ["keyword 1", "keyword 2"],
"body": "This is dummy text."
}
}

Routes Conventions
Everyone has their own way of structuring the API routes, the function names of the APIs, and HTTP methods. Whatever method you use, just make sure to stay consistent.
But, there is a common way that Rails implies on its developers, remember Convention over Configuration! If we keep our example in mind, the first 5 APIs are an ideal way of writing APIs according to Rails.

GET — index -> Fetches all Blogs
POST — create -> Creates a Blog
GET /:blog_id — show -> Fetches a Blog by ID
PATCH /:blog_id — update -> Updates a Blog by ID
DELETE /:blog_id — delete -> Deletes a Blog by ID
We don’t even need to write the above routes. Ruby on Rails gives us an easier way to achieve the same result!

resources
in routes.rb
implies all the routes mentioned previously.
These are just conventions though, keep in mind that you can use your own way of doing things. The idea is that if every developer follows the same conventions then the codebase automatically becomes more readable and maintainable.
Must-have Gems
Parameter Validation: rails-param
As discussed already, this Gem helps us to easily add robust parameter validation to our APIs.
Conversion of cases: oj
This is a Gem that solves the never-ending war between Frontend & Backend developers :) Typically, the case convention followed in Frontend is camelCase
and the case convention followed in Backend is snake_case
.
Once oj
is configured, it acts as an interceptor that can change the case of every key inside the incoming request and also change the case for every key that is sent as an outgoing response.
Enforcing best coding practices: rubocop
As the name suggests, Rubocop is a Gem that can be used for enforcing best coding practices in a project. It also provides the ability to configure which practices to enforce and which practices not to enforce.
Authentication: devise & devise-jwt
These Gems help us not worry about handling user authentication, token expiry logic, email validation, and much more!
Debugger: pry
Great Gem to debug APIs in real-time and find those tricky bugs!
N+1 Queries Finder: bullet
This is a Gem that finds N+1 queries in the APIs and also recommends how we can solve the problem.
Conclusion
At the end of the day, velocity is what matters while developing products, may it be any framework or tool. But at the same time, we should not overlook the quality of the code we write. I know that in some cases we cannot follow all the best practices, but we should try to implement as many as possible or at least add TODO statements with the solution so that it can be solved later.

Thank you for making it this far! Hopefully, you learned a thing or two after reading this article. I also hope that you rush to your codebase and try to implement these best practices and find inner peace.
If you would like to refer to the codebase that was used for the examples shown in this article, here is the GitHub repository.