Rails is a powerful and relatively easy to use framework. Its dominant
principles heavily emphasize convention over configuration, allowing new Rails applications to be up and running in a fraction of the time for applications created in many other, more verbose languages. However, the simplicity that Rails provides can also be a pitfall, particularly for newer developers just getting into the design space. In this article we’ll examine a handful of some of the most common mistakes in Rails development, and provide guidance on how these can best be avoided in your own coding adventures, so let’s get to it!
Application Logic Inside Views
Rails is, by default, a model-view-controller (
MVC) framework. While other similar
MV_ patterns have been introduced and become more popular as of late, the fundamental nature of Rails is to rely on
models to handle business logic,
views to render visual components to the user, and
controllers to manage the connection between the other two.
Most developers strongly encourage data access logic inside your
models, leaving your
views free to focus purely on the layout and visual aspect of your application. Unfortunately,
views are typically written using a templating engine such as
ERB, which is the default selection for new Rails applications.
ERB and the like allow inline Ruby within the
ERB file itself. This often leads to the common mistake that some newer Rails developers make: including application logic inside
For example, consider this snippet of the
views/books/index.html.erb file, which displays a header indicating the
title property of the
favorite_book variable, which stores the user’s favorite book. However, if
favorite_book doesn’t exist, the
view cannot display the title, so it defaults to
<h2> My Favorite Book: <% if favorite_book %> <%= favorite_book.title %> <% else %> UNKNOWN <% end %> </h2>
There are two issues going on here. The first is that there is application logic occurring within the
view, which can be difficult to manage as the application grows, not to mention a nightmare to debug. The second issue is that we’re effectively using a constant, hard-coded value of
UNKNOWN, which is never a good idea.
To improve this simple example we’ll move the majority of the logic out of the
view file and into a
controller — in this case, as a
helper_method. Normally this method shouldn’t be in the
ApplicationController, but we’re throwing it in there for the sake of this example. Either way, we can now perform logic to check if the
favorite_book variable exists:
# book.rb class Book < ApplicationRecord DEFAULT_TITLE = 'UNKNOWN'.freeze end # application_controller.rb class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :favorite_book def favorite_book @favorite_book ||= Book.find session[:favorite_book_id] if session[:favorite_book_id] if @favorite_book @favorite_book else Book.new(title: Book::DEFAULT_TITLE) end end end
Now we have the
#favorite_book helper method that attempts to find the
Book in the database based on the
session[:favorite_book_id] value, if it exists. If not, a new
Book instance is created and the
title is set to the default value of
UNKNOWN. With the logic moved out of the
views/books/index.html.erb code has been dramatically simplified:
<h2>My Favorite Book: <%= favorite_book.title %></h2>
Now we don’t need to worry about whether the user has already picked a favorite book or not inside our
view. Instead, we just access the
title property no matter what, and the appropriate value is returned regardless.
Improper Predicate Method Usage
Another common mistake during Rails development is improperly using predicate methods. For those unaware, a
predicate method is any method that syntactically ends with a question mark (
?) and that should return only a
truthy result. A
truthy result is commonly known as
false, but it’s important to understand the slight difference in some potential values. Specifically, both
nil are considered
truthy-false values in Ruby, meaning both will effectively be considered a false or negative result when returned by a predicate method. Any other value will be considered
truthy-true, which is effectively indicates
true or a positive result.
When creating your own
predicate methods, it’s important to understand their main purpose. To that end, a predicate method should never perform any action or alter any data. For example, this predicate method determines if the favorite book is currently within the library, and, if not, it changes the
book.library property to
def favorite_book_in_library? book = favorite_book # Confirm that book is in library. unless book.library # If not, add library. book.library = true true end end
This is a direct manipulation of data that should never occur in a
predicate method. Instead, the predicate should only examine existing data and return a boolean result. In the case above, we should remove the
book.library assignment and return the result of the property check. We can accomplish this with a single line:
# Check if favorite book is in library. def favorite_book_in_library? return favorite_book.library end
The phrase “fat model, skinny controller” is a well-known and often divisive best practice. It suggests that your
models should be sizeable and contain the majority of your domain logic, while your
controllers should remain small and only perform the basic functionality of linking
views. However, as Rails has matured and been used for larger and more complex applications, the notion that a
fat model is always the proper avenue to take has fallen by the wayside.
ActiveRecord (which is now technically
ApplicationRecord, which extends
ActiveRecord) is the base class for all
models in Rails. The purpose of
ActiveRecord is to closely map to the underlying data objects in a persistent storage medium; most commonly a database. In fact,
ActiveRecord is itself an implementation of an
object relational mapping (
As your application grows in size and complexity, it can be all too easy to throw any and all business logic into the
models and call it a day. Obviously, you don’t want to put extra logic into the
views, so the
model is the most obvious dumping ground for most newer Rails developers. However, given the tight integration between the data layer and your
models, its easy to quickly fall into the trap of enormous
models that go well beyond the “single responsibility” principle they should focus on. After all, every
model is created and named after a corresponding data table in the database, so when a
model extends its functionality and logic outside the scope of that particular domain, things start to get messy.
The question then remains: How do we avoid obese
models? There are a few schools of thought, but the simplest answer is to vigilantly refactor your existing
models to keep them thin and relevant to their singular responsibility. This can be accomplished through the extraction and creation of a number of secondary object types:
Value Objects– Immutable classes that are compared using their value rather than their identity.
Service Objects– Classes that encompass complex behaviors.
Service objectsoften make use of multiple
modelsat once, lending themselves to the notion that no single
modelshould “own” this logic or behavior.
Form Objects– Classes which manipulate multiple
modelswhen a user submits a form. These are often referred to as
Query Objects– Classes that handle complicated database queries.
There are many more options available to trim down extraordinarily fat
models. In fact, we’ll go into much greater detail and provide code examples in a future article dedicated to refactoring obese models in Rails, so be on the lookout for that!
Extreme Reliance on Gems
Most Rails developers have gone through an all-too-familiar experience at some point in their career. As you’re getting into Rails and discovering all the magical things it can do, you find the plethora of gems and their rich ecosystem, which supports millions of developers around the world. Gems can accomplish so much, with so little effort. Need to solve a particular problem? Chances are there’s already a gem for that. But beware! The call of the RubyGems repository is a dangerous siren song if not properly considered and managed.
The biggest potential drawback when adding a new gem to your application is application-breaking bugs. While smart developers will only use the most stable and well-tested gems, even those can cause unexpected behaviors or have their own bugs that slip through the cracks.
Putting aside the potential that the gem itself is flawed in some way, adding a gem to perform a task is often more resource intensive than if you were to code the solution yourself. A gem that can help your application perform one task is often going to provide additional functionality (and thus, resource usage) that your application simply doesn’t need. For example, if you need an easy way to parse some XML data you might consider the popular
nokogiri gem, which is a powerful parsing API that has been around for many years now. While your application may only ever use a small portion of a gem’s full power, you’re often still loading the entirety of the gem and uses resources that you could be saving or using elsewhere.
Lastly, when your
Gemfile indicates a new gem dependency required by your application, that gem itself often includes gem dependencies of its own, which may, in turn, have their own dependencies, and so on. Again, the better gems will avoid dependencies as much as possible, but if you aren’t particularly careful, your application can quickly grow from a couple dozen base gems for most Rails applications to hundreds in a very short period of time.
The bottom line is, think carefully before adding gems that you may not need. Whenever possible, roll your own coded solution to perform tasks that are specific to your business requirements. This will give you better control over how those tasks are performed, while also dramatically improving debugging capabilities when something (invariably) goes wrong.
Directly Calling External Services
Another common mistake when first getting into Rails developer is making direct application calls to third-party services. For example, when a new user signs up and provides their telephone number, your application may use a third-party service, like
Twilio, to automatically text the user a multi-factor authentication confirmation code. The relevant code might look something like this
require 'twilio-ruby' def send_authentication_code(number, code) account_sid = "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # Your Account SID from www.twilio.com/console auth_token = "your_auth_token" # Your Auth Token from www.twilio.com/console @client = Twilio::REST::Client.new account_sid, auth_token message = @client.messages.create( body: code, to: number, from: ENV['TWILIO_NUMBER']) puts message.sid end
#send_authentication_code method is directly called in your application, you risk your application hanging while the third-party service is contacted and the request is executed.
To avoid potentially hanging your application while accessing outside services, it’s best to integrate a job system that allows a queue to store and execute work behind the scenes, independent of your main application threads. In fact, Rails 4.2 introduced the
ActiveJob class, which was specifically built for this purpose and can be easily integrated with other popular queuing backends like
Resque, and more.
Check out Airbrake’s Rails exception handling gem, which simplifies the error-reporting process for all of your Ruby web framework projects, including
Rack, and more. Built on top of Airbrake’s powerful and robust
Airbrake provides your team with real-time error monitoring and reporting across your entire application. Receive instant feedback on the health of your application, without the need for user-generated reports or filling out issue tracker forms. With built-in integration for Ruby web frameworks,
Heroku support, and even job processing libraries like
Airbrake can be integrated into your application and begin revolutionizing your debugging workflow in just a few minutes. Try Airbrake today with a free 30-day trial.