Ruby on Rails is a web application framework, we all know that. Rails makes us more productive and lets us focus on the task at hand rather than the technology. But sticking to best practices in Rails, especially when starting out, is very important. In this post, we’re going to look at some of the best practices in Ruby on Rails.
The Road to Ruin
If you neglect the best practices of a web application framework, you’re running the risk of drifting out of the framework without even knowing it. In the worst cases, your applications starts to become a nightmare for you to handle, making it difficult to develop new features, maintain the project, or bring in new developers. Trust me, stick to the best practices to remain effective and efficient, rather than having people (i.e., your team) pulling their hair out.
The Path to Glory
The name says it all: Best practices. They are the best and most widely used for a reason. Here’s some benefits:
- Maintainability
- Readability
- Elegance
- Faster development
- DRY code
Let’s get started.
Follow the Ruby on Rails community style guidelines:
In every programming language, we have seen both ugly and beautiful code. Code style varies from person to person, which can lead to a delay when bringing on new developers. Having a community-driven style guide is very important, as it plays a vital role in enforcing a consistent style throughout a code base. Projects are usually built in small to large teams, with people coming from different programming backgrounds and styles. Following the Ruby community style guide is my #1 best practice and here are some of the style preferences I really like to emphasize:
Two Space Indentation
This is one of the most widely adapted and agreed upon style guidelines in the Ruby community. Use 2 space indentation instead of 4 space indentation. Let’s see an example:
4 space indentation
def some_method
some_var = true
if some_var
do_something
else
do_something_else
end
end
2 spaces indentation
def some_method
some_var = true
if some_var
do_something
else
do_something_else
end
end
The latter is much cleaner and readable. Also, it will be more evident in a larger file with more levels of indentation.
Define Predicate Methods with a ?
In Ruby, we have a convention for the methods which returns true
or false
. These methods are also known as predicate methods and the convention is end there name with a question mark (?
). In most programming languages, you’ll see methods or variable names defined like is_valid
or is_paid
, etc. Ruby discourages this style and encourages them in a more human language way like object.valid?
or fee.paid?
(notice no is_
prefix, as well) keeping inline with idiomaticness and readability of Ruby.
Iteration: Use each
Instead of for
Almost all Ruby programmers use each
instead of a for
when iterating through a collection. It is simply more readable.
* for *
for i in 1..100
...
end
* each *
(1..100).each do |i|
...
end
See?
Conditionals: Use unless
Instead of !if
:
If you find yourself using an if
statement with a negative condition i.e:
if !true
do_this
end
or
if name != "sarmad"
do_that
end
then you should use Ruby’s exclusive unless
. Like this:
unless true
do_this
end
or
unless name == "sarmad"
do_that
end
Again, it’s about readability. However, if you need to involve an else
to your conditional, never use unless-else
.
Bad
unless user.save
#throw error
else
#return success
end
Good
if user.save
#return success
else
#throw error
end
Short Circuits**
Short circuit is the term used for early exits from methods under certain conditions. Consider this example:
if user.gender == "male" && user.age > 17
do_something
elsif user.gender == "male" && user.age < 17 && user.age > 5
do_something_else
elsif user.age < 5
raise StandardError
end
In this case, it needs to go through all conditions to find out if a user is below 5 years of age and raise an exception. The preferred way is:
raise StandardError if user.age < 5
if user.gender == "male" && user.age > 17
do_something
elsif user.gender == "male" && user.age < 17 #we saved a redundant check here
do_something_else
end
It is more effective to return early when a certain condition is met.
Tip: I highly recommend you take a detailed look at the style guides here (Ruby) and here (Rails).
Write Tests
If you are familiar with Rails, then you know how much emphasis the Rails community puts on testing. I’ve heard people say that, as a newbie, testing makes it difficult to learn Rails. Also, some say it makes sense to get on with the basics of Rails (or in some cases general web development) first. But it doesn’t stop testing from being an absolute best practice in software development. As a matter of fact, I’ve seen people complaining that it takes more time to complete a feature then it requires when you take the testing route. But once they’ve hopped on testing Rails, and put up with the “hassle” of writing tests first, they actually started building features in no time. Plus, it also covers so many of the edge cases that it drives out a much better design of our objects. A good Ruby developer is instinctively good at testing.
Let’s list some benefits of testing:
- Tests acts as detailed specifications of a feature or application.
- Tests acts as a documentation for other devs, which helps them understand your intent in an implementation.
- Tests helps in catching and fixing bugs beforehand.
- Tests gives you confidence when refactoring code or making performance enhancements that nothing is broken as a result.
DRY (Don’t Repeat Yourself)
Do whatever it takes to make sure that you don’t repeat yourself, avoiding duplication as much as you can. Let’s discuss a couple of the ways Ruby’s object oriented principles can help avoid duplication.
Use Abstract classes: Consider you have these two classes:
class Mercedes
def accelerate
"60MPH in 5 seconds"
end
def apply_brakes
"stopped in 4 seconds"
end
def open_boot
"opened"
end
def turn_headlights_on
"turned on"
end
def turn_headlights_off
"turned off"
end
end
class Audi
def accelerate
"60MPH in 6.5 seconds"
end
def apply_brakes
"stopped in 3.5 seconds"
end
def open_boot
"opened"
end
def turn_headlights_on
"turned on"
end
def turn_headlights_off
"turned off"
end
end
We have three duplicate methods open_boot
, turn_headlights_on
, and turn_headlights_off
. We’re not gonna discuss why we shouldn’t have code duplication in this post, you can read about that here. For now, it’s just against the DRY principle. Best practice here is to use class inheritance and/or abstract classes. Let’s rewrite our class to solve the problem.
class Car
# Uncomment the line below if you want this class to be uninstantiable
# i.e you can't make an instance of this class.
# You can only inherit other classes from it.
# self.abstract = true
def open_boot
"opened"
end
def turn_headlights_on
"turned on"
end
def turn_headlights_off
"turned off"
end
end
class Mercedes < Car
def accelerate
"60MPH in 5 seconds"
end
def apply_brakes
"stopped in 4 seconds"
end
end
class Audi < Car
def accelerate
"60MPH in 6.5 seconds"
end
def apply_brakes
"stopped in 3.5 seconds"
end
end
Got the idea? Much better!
Use Modules
Modules, on the other hand, are a flexible way to share behavior across classes. The reasons one would use modules (composition) over inheritance are beyond the scope of this post. Suffice it to say that modules are a “has-a” and inheritance is a “is-a” relationship between classes and behavior:
class Newspaper
def headline
#code
end
def sports_news
#code
end
def world_news
#code
end
def price
#code
end
end
class Book
def title
#code
end
def read_page(page_number)
#code
end
def price
#code
end
def total_pages
#code
end
end
Let’s say we need to add a print
method to both classes without duplicating the code. Use a module, like this:
module Printable
def print
#code
end
end
Modify the classes to make them include this module:
class Newspaper
#This wil add the module's methods as instance methods to this class
include Printable
def headline
#code
end
def sports_news
#code
end
def world_news
#code
end
def price
#code
end
end
class Book
#This wil add the module's methods as instance methods to this class
include Printable
def title
#code
end
def read_page(page_number)
#code
end
def price
#code
end
def total_pages
#code
end
end
This is a very powerful and useful technique. We can also add a module’s methods as class methods by using extend Printable
instead of include Printable
.
Smart Use of Enums
Say you have a model named Book
and a column/field in which you want to store the status of whether it’s a draft, completed, or published. You find yourself doing something like this:
if book.status == "draft"
do_something
elsif book.status == "completed"
do_something
elsif book.status == "published"
do_something
end
or
if book.status == 0 #draft
do_something
elsif book.status == 1 #completed
do_something
elsif book.status == 2 #published
do_something
end
You should look at Enums! Define the status
column to be integer, ideally not null (null: false
), and a default value of the status you want your model to have after creation, i.e default: 0
. Now, define enums in your model like this:
enum status: { draft: 0, completed: 1, published: 2 }
Now you can rewrite the code as:
if book.draft?
do_something
elsif book.completed?
do_something
elsif book.published?
do_something
end
Looks great, doesn’t it? Not only it gives you these predicate methods with names, it also gives you the methods to switch between defined statuses.
book.draft!
book.completed!
book.published!
These methods will as switch the status matching the method. What an elegant tool to have in your arsenal.
Fat Models, Skinny Controllers and Concerns
Another best practice is to keep non-response related logic out of the controllers. Examples of code you don’t want in a controller are any business logic or persistence/model changing logic. For example, someone might have their controller like:
class BooksController < ApplicationController
before_action :set_book, only: [:show, :edit, :update, :destroy, :publish]
# code omitted for brevity
def publish
@book.published = true
pub_date = params[:publish_date]
if pub_date
@book.published_at = pub_date
else
@book.published_at = Time.zone.now
end
if @book.save
# success response, some redirect with a flash notice
else
# failure response, some redirect with a flash alert
end
end
# code omitted for brevity
private
# Use callbacks to share common setup or constraints between actions.
def set_book
@book = Book.find(params[:id])
end
# code omitted for brevity
end
Let’s move this complicated logic to the relevant model instead:
class Book < ActiveRecord::Base
def publish(publish_date)
self.published = true
if publish_date
self.published_at = publish_date
else
self.published_at = Time.zone.now
end
save
end
end
class BooksController < ApplicationController
before_action :set_book, only: [:show, :edit, :update, :destroy, :publish]
# code omitted for brevity
def publish
pub_date = params[:publish_date]
if @book.publish(pub_date)
# success response, some redirect with a flash notice
else
# failure response, some redirect with a flash alert
end
end
# code omitted for brevity
private
# Use callbacks to share common setup or constraints between actions.
def set_book
@book = Book.find(params[:id])
end
# code omitted for brevity
end
This was a straightforward case where it is clear that this piece of functionality belongs to the model. There are lots of other cases where you have to be smart to find the right balance and know what should go where. Sometimes, logic you take out from a controller doesn’t fit into the context of any model. You have to figure out where would it fit the best. I’ll try to lay down some simple rules for you according to my experience. If you think there’s a better approach to some problem, let me know in the comments.
-
Controllers should only make simple queries to the model. Complex queries should be moved out to models and broken out in reusable scopes. Controllers should mostly contain request handling and response related logic.
-
Any code that is not request and response related and is directly related to a model should be moved out to that model.
-
Any class which represents a data structure should go into the app/models directory as a Non-ActiveRecord model (table-less class).
-
Use PORO (Plain Old Ruby Objects) Ruby classes when logic is of a specific domain (Printing, Library & etc.) and doesn’t really fit the context of a model (ActiveRecord or Non-ActiveRecord). You can put those classes in app/models/some_directory/. Anything inside the app/ directory is automatically loaded on app startup as it’s include in the Rails autoload path. POROs can also be placed in app/models/concerns & app/controllers/concerns directories.
-
Place your PORO, Modules, or Classes in lib/ directory if they are application independent and can be used with other applications as well.
-
Use modules if you have to extract out common functionality from otherwise unrelated functionality. You can place them in app/* directory and in lib/ directory if they are application independent.
-
The “Service” layer is another really important place in supporting vanilla MVC when the application code is growing and it’s getting hard to decide where to put specific logic. Imagine you need to have a mechanism to send SMS or Email notifications to some subscribers when a book is published, or a push notification to their devices. You can create a Notification service in app/services/ and start service-ifying your functionality.
Internationalization/Localization
Internationalize your app from the beginning. Don’t leave it for later or it will only grow to haunt you later. Good websites can’t rely on a single language, they usually have a bigger lingual target. The bigger, the better. It is one of the best practices to internationalize along with development. That’s why Rails ships with the I18n gem, which signifies the importance of internationalising your app. You can read more about it here.
It gives you following out of the box:
- Support for English and similar languages out of the box
- Making it easy to customize and extend everything for other languages
It lets you set a default locale and change the locale according the user’s location or preference.
Here is short example conversion of a non-internationalized HTML code to an internationalized one:
<h1>Books Listing</h1>
<table>
<thead>
<th>Name</th>
<th>Author</th>
</thead>
<tbody>
<td> Some book </td>
<td> Some author </td>
</tbody>
</table
Files in the config/locales directory are used for internationalization and are automatically loaded by Rails. Every Rails app has a config/locales/en.yml by default. This file is responsible for holding English translations. If you need to add translations for more languages you can just add the files matching the locale name with .yml extension. In this example we’ll stick to en.yml. Letrs refactor the above HTML to use internationalization:
<h1><%= t('.title') %></h1>
<table>
<thead>
<th><%= t('.name') %></th>
<th><%= t('.author') %></th>
</thead>
<tbody>
<td> Some book </td>
<td> Some author </td>
</tbody>
</table>
Now place the content to show in the .yml file so that the updated HTML can extract the translations.
# config/en.yml
en:
title: "Books Listing"
name: "Name"
author: "Author"
Database Best Practices
The db/schema.rb file comes with a comment at its top which says:
It's strongly recommended that you check this file into your version control system.
and
If you need to create the application database on another system, you should
be using db:schema:load, not running all the migrations from scratch. The
latter is a flawed and unsustainable approach (the more migrations you'll
amass, the slower it'll run and the greater likelihood for issues).
It’s strongly recommended to always check this file into your version control system. If this file is not checked in and not up to date, then you can’t utilize rails db:schema:load
. As it is explained above, if you need to create the application database on another machine then you should use db:schema:load
instead of db:migrate
. Running all migrations from scratch is discouraged due its tendency to be get flawed over time. I’ve personally experienced this problem a number of times. When migrations are buggy, it’s hard to track down where the problems lie, where it went wrong with the migrations. db:schema:load
is the saviour in those situations.
Beware! db:schema:load
is to be used just when you need to create your application database on a new system. If you are adding new migrations, you should simply just let db:migrate
do the work. If you run db:schema:load
on an existing and populated DB, your data (could be production data) will be wiped out. So just remember these three simple rules and you’ll be fine:
- Always check schema.rb into your version control system when you add and apply any new migrations.
- Use
db:schema:load
when creating the application database on a new system. - Use
db:migrate
in all other cases when you need to apply the newly added migrations.
Tip: Don’t use migrations to add data to the DB. Use db/seeds.rb for that purpose instead.
Nested Resources/Routes
If you have a resource which belongs to another resource, then it’s a good idea to define the routes of the child resource nested within the routes of parent resource. For example, if you have a Post resource and a Comment resource, and have these model associations set up:
- Post model has many comments
- Comment model belongs to Post
And your config/routes.rb file looks like this:
resources :posts
resources :comments
This will define your routes like this:
- http://localhost:3000/posts
- http://localhost:3000/posts/1
- http://localhost:3000/posts/1/edit
- http://localhost:3000/comments
- http://localhost:3000/comments/1
- http://localhost:3000/comments/1/edit
Which is OK, but not a good practice. We should define the Comment routes nested within Post routes. This is how:
resources :posts do
resources :comments
end
Now this will define your routes like this:
- http://localhost:3000/posts
- http://localhost:3000/posts/1
- http://localhost:3000/posts/1/edit
- http://localhost:3000/posts/1/comments
- http://localhost:3000/posts/1/comments/1
- http://localhost:3000/posts/1/comments/1/edit
The URLs are readable and indicate that the comments belong to a Post whose ID is 1. There’s a slight gotcha though: You must make some changes in your usage of Ruby’s form and URL helpers. For instance, in a comment form, you have this:
<%= form_for(@comment) do |f| %>
<!-- form elements removed for brevity -->
<% end %>
This will need to be changed. Currently it takes a new instance of Comment, we need to change it to something like this:
<%= form_for([@comment.post, @comment]) do |f| %>
<!-- form elements removed for brevity -->
<% end %>
Pay attention to the argument passed in the form_for
helper. It’s now an array, and contains the parent resource first and the Comment instance second.
Another thing which we’ll need to change is all the URL helpers for Comments:
<%= link_to 'Show', comment %>
Will be changed like this:
<%= link_to 'Show', [comment.post, comment] %>
And your show link will work. Let’s take a look at Edit link:
<%= link_to 'Edit', edit_comment_path(comment) %>
This will be changed like this:
<%= link_to 'Edit', edit_post_comment_path(comment.post, comment) %>
Pay attention! Both the helper name (edit_post_comment_path
) and arguments (2 args instead of 1) are changed in order to make it work with nested resources/routing.
Use Time.zone.now
Instead of Time.now
A best practice is to always define default timezone of your application in config/application.rb like this:
config.time_zone = ‘Eastern Time (US & Canada)'`.
Date.today
and Time.now
always gives the local date and time of the machine, in the machine’s timezone. It makes sense to use Time.zone.now
and Time.zone.today
to get around the conflicts between timezones on development machines and production servers.
Don’t Put Too Much Logic in Views
Views are the presentation layer, which shouldn’t contain logic. You should avoid having checks like this:
<% if book.published? && book.published_at > 1.weeks.ago %>
<span>Recently added</span>
<% end %>
or
<% if current_user.roles.collect(&:name).include?("admin") || (user == book.owner && book.draft?) %>
<%= link_to 'Delete', book, method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
What you could do is move out this conditional check to a helper module, which are located in app/helpers and are automatically available in all views. Here is an example:
# app/view/helpers/application_helper.rb
module ApplicationHelper
def recently_added?(book)
book.published? && book.published_at > 1.weeks.ago
end
# current_user is defined in application controller, which can be
# accessed from helper modules & methods
def can_delete?(book)
current_user.roles.collect(&:name).include?("admin") || (user == book.owner && book.draft?)
end
end
Modifying the above view markup as:
<% if recently_added?(book) %>
<span>Recently added</span>
<% end %>
and
<% if can_delete?(book) %>
<%= link_to 'Delete', book, method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
There are many other places this can_delete?
method can be used, but this is just an example to separate logic from views.
Conclusion
Like I discussed in the beginning of this post, it makes our lives easier if projects are written the right way as defined by the framework and the community. Best practices of a framework are developed by people who are experienced, who have been through the negatives of doing things in a certain way, driving them to a best practice that goes around the problem. It’s nice for us to have those kind of people in our community and gain from their experience. Luckily Rails is popular for having a great community, which makes it loved the way we do.
I am sure there are other practices that you feel are important. Let me know your favorites in the comments.
Top boilerplates tend to use the industry’s best practices so you can also explore Ruby on Rails boilerplates.
Frequently Asked Questions (FAQs) about Ruby on Rails Best Practices
What are some of the most important best practices for Ruby on Rails?
Ruby on Rails, often simply Rails, is a server-side web application framework written in Ruby. It is a model-view-controller (MVC) framework, providing default structures for a database, a web service, and web pages. Some of the most important best practices for Ruby on Rails include keeping the controller skinny and models fat, using database indexing, following RESTful routes, using background jobs for long tasks, and writing tests for your code. These practices help to ensure that your Rails application is efficient, easy to maintain, and less prone to errors or bugs.
Why is it important to keep the controller skinny and models fat in Rails?
In Rails, the controller is responsible for handling the user’s request and sending a response back to the user. The model is responsible for interacting with the database and performing the business logic. By keeping the controller skinny, we ensure that it only handles the request and response, making it easier to understand and maintain. On the other hand, by making the models fat, we ensure that the business logic and database interaction are encapsulated in the model, making it easier to test and reuse.
How does database indexing improve Rails application performance?
Database indexing is a technique used to speed up the retrieval of records from a database. Without an index, the database server must scan the entire table to find the relevant records, which can be very slow if the table has many records. With an index, the database server can find the relevant records much faster, similar to how an index in a book helps you find information quickly. In Rails, you can add an index to a database table using the add_index method in a migration.
What are RESTful routes and why are they important in Rails?
RESTful routes are a convention in Rails that maps HTTP verbs (get, post, put, delete) to controller actions. They provide a standardized way of structuring the URLs for your application, making it easier for other developers to understand and use your application’s API. RESTful routes also help to ensure that your application adheres to the principles of Representational State Transfer (REST), a software architectural style that emphasizes scalability, statelessness, and the use of standard HTTP protocols.
Why should long tasks be performed in background jobs in Rails?
Long tasks, such as sending emails or processing images, can significantly slow down the response time of your Rails application if they are performed during the request/response cycle. By moving these tasks to background jobs, you can improve the response time of your application and provide a better user experience. Rails provides several ways to create background jobs, including Active Job and Sidekiq.
Why is it important to write tests for your Rails code?
Writing tests for your Rails code helps to ensure that your application works as expected and makes it easier to refactor or add new features to your code. Tests can catch bugs or errors before your code is deployed to production, saving you time and effort in the long run. Rails provides a built-in testing framework, but you can also use other testing libraries like RSpec or Minitest.
How can I ensure that my Rails code is easy to read and maintain?
One of the best ways to ensure that your Rails code is easy to read and maintain is to follow the Ruby style guide and Rails best practices. This includes using meaningful variable and method names, keeping methods short and simple, and organizing your code into modules and classes. You should also comment your code to explain why you are doing something, not what you are doing.
What tools can I use to check the quality of my Rails code?
There are several tools that you can use to check the quality of your Rails code, including RuboCop, Reek, and Brakeman. RuboCop is a Ruby static code analyzer and formatter that can enforce many of the guidelines outlined in the Ruby style guide. Reek is a tool that can detect code smells in Ruby. Brakeman is a security vulnerability scanner specifically designed for Rails applications.
How can I improve the performance of my Rails application?
There are several ways to improve the performance of your Rails application, including database indexing, caching, using background jobs for long tasks, and optimizing your Ruby code. You should also monitor the performance of your application using tools like New Relic or Skylight, so you can identify and fix any performance bottlenecks.
How can I learn more about Ruby on Rails best practices?
There are many resources available to learn more about Ruby on Rails best practices, including books, online tutorials, and documentation. Some recommended books include “The Rails 5 Way” by Obie Fernandez and “Agile Web Development with Rails 6” by Sam Ruby. You can also find many tutorials and articles online on websites like SitePoint, Medium, and the official Rails Guides.
Sarmad is a Rubyist. Works at 10Pearls as a Senior Software Engineer. And an independent Ruby on Rails consultant. A fitness enthusiast who likes to workout and eat well.