Ruby on Rails — Best Practices Every Developer Should Know

Karan Jagtiani
Dev Genius
Published in
10 min readSep 20, 2022

--

Ruby on Rails Best Practices Every Developer Should Know 2022 by Karan Jagtiani

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 :)

  1. Code Reusability
  2. High Velocity
  3. Performance
  4. Maintainability

Contents

  1. Philosophies of Ruby on Rails
  2. Fat-Model-Skinny-Controller
  3. Module Utilization
  4. N+1 Query Problem
  5. Preloading Data
  6. Custom Controller Actions
  7. Parameter Validation
  8. Routes Conventions
  9. Must-have Gems
  10. 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.

Ruby on Rails Best Practices — Database Schema
Database Scheme Followed in this Article

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.

Ruby on Rails Best Practices — Get Frequency of Keywords in a Blog
Get Frequency of Keywords in a Blog Model

This is what the controller would look like.

Ruby on Rails Best Practices — Get Frequency of Keywords Controller
Get Frequency of Keywords Controller

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.

Ruby on Rails Best Practices — Get User Blogs using Helper Function
Get User Blogs using a Helper Function

This is what the controller would look like:

Ruby on Rails Best Practices — Get User Blogs Controller
Get User Blogs Controller

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:

Ruby on Rails Best Practices — N+1 Query Problem
Save Blogs — N+1 Query Problem

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:

Ruby on Rails Best Practices — N+1 Query Solution
Bulk Save Blogs — N+1 Query Solution

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.

Ruby on Rails Best Practices — Not Preloading Data
Get User Blogs — Not Preloading data

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.

Ruby on Rails Best Practices — Not Preloading Data Console Output
Query Logs before Preloading

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

Ruby on Rails Best Practices — After Preloading the Data
Get User Blogs — Preloading the Data

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:

Ruby on Rails Best Practices — After Preloading the Data Console Output
Query Logs after Preloading

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?

Ruby on Rails Best Practices — Eager Loading the Data
Get User Blogs — Eager Loading the Data

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

Ruby on Rails Best Practices — Eager Loading the Data Console Output
Query Logs after Eager Loading

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:

  1. Get Blog by ID
  2. Update Blog by ID
  3. 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.

Ruby on Rails Best Practices — Custom Controller Action
Custom Controller Action

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:

  1. Helps other developers not make mistakes and break the code during development.
  2. 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."
}
}
Ruby on Rails Best Practices — Parameter Validation
Parameter Validations

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.

Ruby on Rails Best Practices — Routes Conventions
Routes Conventions

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!

Ruby on Rails Best Practices — Implied Routes Conventions
Rails Implied Routes Conventions

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.

Ruby on Rails Best Practices — Inner Peach Oogway Meme

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.

--

--