Buggy Code: 10 Common Rails Programming Mistakes
Editor’s note: This article was updated on 11/30/2022 by our editorial team. It has been modified to include recent sources and to align with our current editorial standards.
While this paradigm has its advantages, it is not without its pitfalls and leaves the door open for developers to make common Rails programming mistakes. The magic that happens behind the scenes in the framework can sometimes lead to developer confusion. It can also have undesirable ramifications with regard to security and performance.
Accordingly, Rails is easy to use—and equally easy to misuse. This tutorial looks at 10 common Ruby on Rails coding problems and shows you how to avoid them and the issues that they cause.
Common Rails Programming Mistake #1: Overloading the Controller With Logic
Rails is based on an MVC architecture. In the Rails community, we’ve been talking about fat model, skinny controller for a while, yet several recent Rails applications I’ve inherited violate this principle. It’s all too easy to move view logic (which is better housed in a helper), or domain/model logic, into the controller.
The problem with this is that the controller object will start to violate the single responsibility principle, making future changes to the codebase difficult and prone to errors.
Generally, the types of logic you should have in your controller are:
- Session and cookie handling. This might include authentication/authorization or other cookie processing.
- Selecting a model. The logic for finding the right model object, given the parameters passed in from the request. Ideally, model selection should be a call to a single find method that sets an instance variable to be used later to render the response.
- Requesting parameter management. Gathering request parameters and calling an appropriate model method to persist them.
- Rendering/redirecting. Rendering the result (HTML, XML, JSON, etc.) or redirecting, as appropriate.
While this pushes the limits of the single responsibility principle, it’s the bare minimum that the Rails framework requires us to have in the controller.
Common Rails Programming Mistake #2: Overloading the View With Logic
The out-of-the-box Rails templating engine, ERB, is a great way to build pages with variable content. However, if you’re not careful, you can soon end up with a large file that is a mix of HTML and Ruby code that can be difficult to manage and maintain. This can also lead to repetition and violation of the Don’t Repeat Yourself (DRY) principle.
This unwieldy mix of HTML and Ruby can manifest itself in a number of ways, one of which is the overuse of conditional logic in views. As a simple example, consider a case in which we have a
current_user method available that returns the currently logged-in user. Often, in such a case, we would wind up with conditional logic structures such as this in view files:
<h3> Welcome, <% if current_user %> <%= current_user.name %> <% else %> Guest <% end %> </h3>
In lieu of embedding conditional logic in our HTML, it would be better to make sure the object returned by
current_user is always set—whether or not someone is logged in (i.e., null if someone is not logged in)—and that view methods can handle a null user reference. For instance, you might define the
current_user helper in
app/controllers/application_controller like this:
require 'ostruct' helper_method :current_user def current_user @current_user ||= User.find session[:user_id] if session[:user_id] if @current_user @current_user else OpenStruct.new(name: 'Guest') end end
This would then enable you to replace the previous view code example with this one simple line of code:
<h3>Welcome, <%= current_user.name -%></h3>
Here are a couple of additional recommended Ruby on Rails coding best practices:
- Use view layouts and partials appropriately to encapsulate repetitive items on your pages.
- Use presenters/decorators like the Draper gem to encapsulate view-building logic in a Ruby object. You can then add methods that perform logical operations into this object—instead of putting these into your view code.
Common Rails Programming Mistake #3: Overloading the Model With Logic
Given the guidance to minimize the logic in views and controllers, the only place left in an MVC architecture to put all that logic would be in the models, right?
Well, not quite.
Many Rails developers actually make this mistake and store everything in their
ActiveRecord model classes, which leads to Mongo files that not only violate the single responsibility principle, but are also a maintenance nightmare.
Functionality—such as generating email notifications, interfacing to external services, converting to other data formats, and the like—doesn’t have much to do with the core responsibility of an
ActiveRecord model, which should be doing little more than finding and persisting data in a database.
So—if the logic shouldn’t go in the views, and it shouldn’t go in the controllers, and it shouldn’t go in the models—well, where should it go?
Enter Plain Old Ruby Objects (POROs). With a comprehensive framework like Rails, newer developers might be reluctant to create their own classes outside of the framework. However, moving logic out of the model into POROs is often just what the doctor ordered to avoid overly complex models. With POROs, you can encapsulate things like email notifications or API interactions into their own classes, rather than housing them into an
So with that in mind, generally speaking, the only logic that should remain in your model is:
ActiveRecordconfiguration i.e., relations and validations.
- Simple mutation methods to encapsulate updating and saving a handful of attributes in the database.
Access wrappers to hide internal model information (e.g., a
full_namemethod that combines
last_namefields in the database).
Sophisticated queries that are more complex than a simple
find; generally speaking, you should never use the
wheremethod, or any other query-building method like it, outside of the model class.
Common Rails Programming Mistake #4: Using Generic Helper Classes as Dumping Grounds
This mistake is a corollary to the preceding mistake number 3. The Rails framework places an emphasis on the named components of an MVC (i.e., model, view, and controller) framework. While definitions exist to describe what we can place in the classes of each of these components, there are times when the methods we develop do not seem to fit into any of the three.
A Rails generator conveniently builds a helper directory and a new helper class to go with each new resource we create. It can be all too tempting to stuff any functionality that doesn’t fit into the model, view, or controller into a helper class.
While Rails is certainly MVC-centric, there is nothing to prevent you from creating your own types of classes and adding appropriate directories to contain the code for those classes. When you develop new functionality, think about which methods group together well and assign descriptive names to the classes that hold those methods. Using a comprehensive framework like Rails is not an excuse to let good object-oriented design best practices go by the wayside.
Common Rails Programming Mistake #5: Too Many Gems
Both Ruby and Rails are supported by a rich ecosystem of gems that collectively provide just about any capability a developer can think of—great for building up a complex application quickly. However, I’ve also seen many bloated applications whose
Gemfile is disproportionately large compared with the functionality it offers.
Excessive use of gems makes the size of a Rails process larger than it needs to be, slowing down performance in production. In addition to user frustration, this can also result in the need for larger server memory configurations and increased operating costs.
An overabundance of gems can cause a heftier Rails application to take longer to start, which makes development slower and makes automated tests take longer (and as a rule, slow tests simply don’t get run as often).
Bear in mind that each gem you bring into an application may, in turn, have dependencies on other gems, and those may also have their own dependencies. Adding gems can thus have a compounding effect. For instance, adding the
rails_admin gem will bring in 11 more gems in total—a 10% increase from the base Rails installation.
As of this writing, a fresh Rails 7.0.4 install includes dozens of gems in the
Gemfile.lock file. This is more than is included in
Gemfile and represents all the gems that the handful of standard Rails gems brings in as dependencies.
Carefully consider whether the extra overhead is worthwhile as you add each gem. As an example, developers will often add the
rails_admin gem that provides a nice web front end to the model structure. But this gem isn’t much more than a fancy database browsing tool. Even if your application requires admin users with additional privileges, you probably don’t want to give them raw database access. You’d be better served by developing your own, more streamlined administration function.
Common Rails Programming Mistake #6: Ignoring Log Files
While most Rails developers may be aware of default log files, a woefully small proportion actually heed the information in those files. In production, tools such as Honeybadger or New Relic can handle log monitoring for us. However, it is crucial to keep an eye on log files as we develop and test our applications.
Much of Rails’ magic happens in the application’s models. By defining associations in your models, it is easy to pull in relations and have everything available to your views. All the SQL needed to fill up your model objects is generated for you. That’s great. But how do you confirm that the SQL being generated is efficient?
One example you will often run into is the N+1 query problem. While impossible to observe in real time, the N+1 query problem is well documented and detectable by reviewing our logged SQL queries.
Say, for instance, you have the following query in a typical blog application where you will be displaying all comments for a select set of posts:
def comments_for_top_three_posts posts = Post.limit(3) posts.flat_map do |post| post.comments.to_a end end
The log file of a request that calls this method might reveal that a single query gets three post objects, and is followed by three more queries to get each object’s comments:
Started GET "/posts/some_comments" for 127.0.0.1 at 2014-05-20 20:05:13 -0700 Processing by PostsController#some_comments as HTML Post Load (0.4ms) SELECT "posts".* FROM "posts" LIMIT 3 Comment Load (5.6ms) ELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 1]] Comment Load (0.4ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 2]] Comment Load (1.5ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 3]] Rendered posts/some_comments.html.erb within layouts/application (12.5ms) Completed 200 OK in 581ms (Views: 225.8ms | ActiveRecord: 10.0ms)
ActiveRecord’s eager loading capability in Rails makes it possible to significantly reduce the number of queries when you specify in advance all the associations that are going to be loaded. This is done by calling the
preload) method on the Arel (
ActiveRecord::Relation) object being built. With
ActiveRecord ensures that all of the specified associations are loaded using the minimum possible number of queries; e.g.:
def comments_for_top_three_posts posts = Post.includes(:comments).limit(3) posts.flat_map do |post| post.comments.to_a end end
If we check the log file after executing our revised code, we will see that comments for all of the post objects were collected in a single query:
Started GET "/posts/some_comments" for 127.0.0.1 at 2014-05-20 20:05:18 -0700 Processing by PostsController#some_comments as HTML Post Load (0.5ms) SELECT "posts".* FROM "posts" LIMIT 3 Comment Load (4.4ms) SELECT "comments".* FROM "comments" WHERE"comments "."post_id" IN (1, 2, 3) Rendered posts/some_comments.html.erb within layouts/application (12.2ms) Completed 200 OK in 560ms (Views: 219.3ms | ActiveRecord: 5.0ms)
Much more efficient. This solution to the N+1 query problem is an example of the kind of inefficiencies that we might find if we peered under the hood of an application in which the developer hadn’t been attentive to the log files.
Reviewing development and test log files during development is a great way to be tipped off to any inefficiencies in your code so that you can correct your work before the application goes into production. And since the dataset you work with in development or testing is likely to be much smaller than one that’s in production, address it as early in the development process as possible. If you’re working on a new app, your production dataset may start out small but, as it grows, Rails will slow down your application.
Consider taking steps to clean your log files of superfluous or inessential information.
Common Rails Programming Mistake #7: Lack of Automated Tests
Ruby and Rails provide powerful automated test capabilities by default, and many Rails developers write sophisticated tests using TDD and BDD methodologies and make use of even more powerful test frameworks with gems like rspec and cucumber.
Despite the ease with which we can add automated testing to a Rails application, I have been unpleasantly surprised by the number of projects I inherited or joined where there were literally no tests written—or, at best, very few. While there is plenty of debate about how comprehensive your testing should be, it is pretty clear that at least some automated testing should exist for every application.
As a general rule of thumb, there should be at least one high-level integration test written for each action in your controllers. If, at some point in the future, a developer extends or modifies the code, or upgrades a Ruby or Rails version, this testing framework will provide them with a clear way of verifying that the basic functionality of the application is working. An added benefit of this approach is that it provides a clear delineation of the full type of functionality provided by the application.
Common Rails Programming Mistake #8: Blocking on Calls to External Services
Third-party providers of Rails services tend to make it very easy to integrate their services into your application via gems that wrap their APIs. But what happens if your external service has an outage or starts running very slowly?
To avoid blocking on such calls, rather than calling these services directly in your Rails application during the normal processing of a request, where feasible, move them to some sort of background job-queuing service. Some popular gems used in Rails applications for this purpose include:
In cases where it is impractical or infeasible to delegate processing to a background job queue, you will need to make sure that your application has sufficient error handling and fail-over provisions for those inevitable situations when the external service goes down or is experiencing problems. You should also test your application without the external service (perhaps by disconnecting your app server from the network) to verify that it doesn’t result in any unanticipated consequences.
Common Rails Programming Mistake #9: Getting Married to Existing Database Migrations
Rails’ database migration mechanism allows you to create instructions to automatically add and remove database tables and rows. Since the files that contain these migrations are named in a sequential fashion, you can play them back from the beginning of time to bring an empty database to the same schema as production. This is a great way to manage granular changes to your application’s database schema and avoid Rails problems.
While it certainly works well at the beginning of your project, as time goes on, the database creation process can start to take longer to complete, and a migration could be misplaced, inserted out of order, or introduced from other Rails applications using the same database server.
By default, Rails creates a representation of your current schema in a file called
db/schema.rb. This file is usually updated when database migrations run. The
schema.rb file can even be generated when no migrations are present by running the
rake db:schema:dump task. A common Rails mistake is to check a new migration into your source repo, without its correspondingly updated
When migrations have gotten out of hand and take too long to run, or no longer create the database properly, you should not be afraid to clear out the old migrations directory, dump a new schema, and continue from there. Setting up a new development environment would then require a
rake db:schema:load rather than the
rake db:migrate that most developers rely on. Additional information on this topic is located in the Rails Guide.
Common Rails Programming Mistake #10: Checking Sensitive Information Into a Source Code Repository
The Rails framework makes it easy to create secure applications that remain impervious to many types of attacks. Some of this is accomplished by using a secret token to secure a session with a browser. Such a token is stored in
config/secrets.yml, and our app reads the token from an environment variable for production servers.
Previous versions of Rails included the token in
config/initializers/secret_token.rb, which is often mistakenly checked into the source code repository with the rest of your application, and, anyone with access to the repository could easily compromise all users of your application.
Make sure that your repository configuration file (e.g.,
.gitignore for git users) excludes the file with your token. Your production servers can then pick up their token from an environment variable or from a mechanism like the one that the dotenv gem provides.
Rails is a powerful framework that hides a lot of the ugly details necessary to build a robust web application. While this makes Rails web application development much faster, developers should pay attention to the potential design and coding errors we’ve identified. This will make your applications more easily extensible and maintainable.
Beware of issues that can make your applications slower, less reliable, or less secure. It’s important to study the framework and make sure that you fully understand the architectural, design, and coding trade-offs you’re making throughout the development process to help ensure a high-quality and high-performance application.
Further Reading on the Toptal Engineering Blog:
- What are the Benefits of Ruby on Rails? After Two Decades of Programming, I Use Rails
- Rails Service Objects: A Comprehensive Guide
- Build Sleek Rails Components With Plain Old Ruby Objects
- Rails 6 Features: What’s New and Why It Matters
- The Publish-Subscribe Pattern on Rails: An Implementation Tutorial
Understanding the basics
What is the main logic behind MVC architecture?
Model, view, controller (MVC) came about as an architecture focused on separation of concerns. The model holds the data for the main application objects. The view should contain only that logic to transform data into a user interface. Controllers respond to the main system events, often tied to application routes, that eventually lead to updated models and views.
How do you use helpers in Ruby on Rails?
Ruby on Rails helpers are functions usually written to share code blocks throughout an application. There are both built-in and custom helpers, and these helpers are used just like functions within an application’s codebase.
What is the purpose of RubyGems?
RubyGems is a package manager for Ruby. Packages contain code used to modify or extend functionality within an application, and may include documentation and utilities.
How do I run a test in Rails?
Rails makes testing simple with the flexibility to run either all tests or a specific one. The bin/rails test command is the method for initiating these tests. If a specific test is not mentioned, then all tests for an application will run.
What is a log file in Rails?
A Rails log file contains the information output by an application while running. The logged information will be limited to the log output level, ranging from high-level errors to verbose debugging information.
What is a Rails service?
A Rails service is an object that allows developers to separate the application’s business logic from the controllers and models. This separation typically allows for simpler controller and model logic.
Why do we need migration in Rails?
Rails migration allows encapsulated database schema changes to be defined and potentially stored in a version control system. Once shared with other developers in a team, these migrations may be run to update data schemas with a command execution.
How secure is Ruby on Rails?
Ruby on Rails has an out-of-the-box default security framework that provides basic protection from common attack vectors.
Located in Newbury Park, United States
Member since December 5, 2013
About the author
Brian is a freelance software developer who delivers a technology value in the corporate world. Expertise with open-source technologies that range from those used in embedded systems to large-scale web applications.