10 Ruby on Rails Best Practices

Share this article

10 Ruby on Rails Best Practices

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.

Key Takeaways

  • Adhere to Community Style Guidelines: Follow the Ruby on Rails community style guidelines to maintain consistency and improve code readability across teams.
  • Emphasize Testing: Prioritize writing tests to ensure code reliability, facilitate feature development, and handle edge cases effectively.
  • Implement DRY Principles: Utilize object-oriented principles and abstract classes to avoid code duplication, enhancing maintainability and clarity.
  • Use Enums for Clarity and Efficiency: Employ enums for managing status fields within models, simplifying code and improving readability.
  • Keep Controllers Skinny: Move business logic and database interactions to models or services to keep controller actions simple and focused on routing.
  • Internationalize from the Start: Integrate internationalization early in development to cater to a global audience, using Rails’ built-in I18n gem for support.

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:

  1. Maintainability
  2. Readability
  3. Elegance
  4. Faster development
  5. 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:

  1. Always check schema.rb into your version control system when you add and apply any new migrations.
  2. Use db:schema:load when creating the application database on a new system.
  3. 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 SabihSarmad Sabih
View Author

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.

best practicesGlennGRuby on Rails
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week