Ruby on Rails is celebrated for its convention-over-configuration approach, enabling developers to build robust applications quickly. However, as applications grow in complexity, maintaining clean, modular, and scalable code becomes a significant challenge. In this blog, we’ll explore the best practices to structure your Rails application effectively and ensure long-term maintainability.
1. Follow the MVC Pattern Closely
Rails is built on the Model-View-Controller (MVC) architecture, which stands for:
- Model: Handles data, logic, and rules of the application.
- View: Responsible for presenting data to the user.
- Controller: Acts as a bridge between the model and the view, processing user input and updating the model or view accordingly.
This separation of concerns helps maintain a clean and organized codebase. For example:
# app/models/user.rb class User < ApplicationRecord validates :name, presence: true end # app/controllers/users_controller.rb class UsersController < ApplicationController def show @user = User.find(params[:id]) end end # app/views/users/show.html.erb <h1><%= @user.name %></h1>
2. Keep Controllers Thin
Fat controllers can lead to chaos in your codebase. A fat controller is one that contains too much business logic, making it hard to read and maintain. To avoid this:
- Use Service Objects to extract business logic:
# app/services/payment_processor.rb class PaymentProcessor def initialize(order) @order = order end def call # Payment processing logic @order.update(status: "paid") end end
- Move reusable logic to concerns or helper classes.
- Use before_action filters for repetitive tasks like authentication or parameter sanitization:
class OrdersController < ApplicationController before_action :set_order, only: [:show, :edit, :update, :destroy] private def set_order @order = Order.find(params[:id]) end end
3. Avoid Fat Models
While models handle business logic, they shouldn’t become unmanageable. To prevent this:
- Use ActiveRecord scopes for query logic:
class Order < ApplicationRecord scope :completed, -> { where(status: 'completed') } scope :recent, -> { order(created_at: :desc) } end
- Extract complex logic into concerns, service objects, or decorators:
# app/models/concerns/trackable.rb module Trackable extend ActiveSupport::Concern included do before_create :set_tracking_code end private def set_tracking_code self.tracking_code = SecureRandom.hex(10) end end
4. Use Concerns Wisely
Concerns allow you to share logic between models or controllers without duplication. However, don’t misuse them as a dumping ground for unrelated code.
Example of a Concern:
# app/models/concerns/archivable.rb module Archivable extend ActiveSupport::Concern def archive update(archived: true) end end
5. Organize Code with Service Objects and Presenters
Service objects handle business logic, while presenters or decorators enhance views with additional functionality. These patterns make your code modular and easier to test.
Example of a Presenter:
# app/presenters/order_presenter.rb class OrderPresenter def initialize(order) @order = order end def formatted_total "#{@order.total_price} USD" end end
6. Break Down the Views
Avoid embedding too much Ruby logic in your views. Use:
- Partials to break down complex views:
<%= render 'shared/header' %>
- Helpers for reusable methods:
module ApplicationHelper def format_price(price) number_to_currency(price, unit: "$") end end
- View Components for encapsulating UI logic:
# app/components/card_component.rb class CardComponent < ViewComponent::Base def initialize(title:, content:) @title = title @content = content end end
<%= render(CardComponent.new(title: "Welcome", content: "Hello, World!")) %>
7. Use the Right Tools for Testing
Testing is crucial for maintaining scalable applications. It ensures your code works as expected and helps prevent bugs when introducing new features or changes.
Why Testing Matters:
- It builds confidence that your application behaves as intended.
- It helps identify edge cases and potential errors early.
- It simplifies debugging by pinpointing failing components.
Simple Example Using RSpec:
Let’s write a test for a User model:
-
Add RSpec to your Gemfile:
gem 'rspec-rails', group: [:development, :test]
Run bundle install and initialize RSpec:rails generate rspec:install
-
Create a test for the User model:
# spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do it "is valid with a name" do user = User.new(name: "Alice") expect(user).to be_valid end it "is invalid without a name" do user = User.new(name: nil) expect(user).not_to be_valid end end
-
Run the test:
bundle exec rspec
This basic example demonstrates how to validate a User model using RSpec. As you grow more comfortable, you can write more complex tests for controllers, views, and integrations.
- RSpec for a robust testing framework:
# spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do it "is valid with valid attributes" do expect(User.new(name: "John")).to be_valid end end
- FactoryBot for creating test data:
# spec/factories/users.rb FactoryBot.define do factory :user do name { "John" } end end
- Capybara for end-to-end testing.
8. Manage Dependencies Wisely
Avoid adding unnecessary gems to your project. Choose well-maintained and widely used gems. Regularly update dependencies and remove unused ones to keep your app lightweight.
9. Use Environment Variables
Sensitive data such as API keys and credentials should never be hardcoded. Instead, use environment variables to manage this data securely. For instance, you can utilize a gem like dotenv to load variables from a .env file or configure Rails credentials for enhanced security.
Example for Beginners:
-
Install the dotenv gem by adding it to your Gemfile and running bundle install:
gem 'dotenv-rails', groups: [:development, :test]
-
Create a .env file in the root directory of your Rails application and add your environment variable:
STRIPE_SECRET_KEY=sk_test_12345
-
Update your application code to use the environment variable:
# config/initializers/stripe.rb Stripe.api_key = ENV['STRIPE_SECRET_KEY']
-
Make sure to add .env to your .gitignore file to avoid committing sensitive data:
echo ".env" >> .gitignore
By following these steps, you can safely manage sensitive configuration data in your Rails application. Use environment variables and tools like dotenv or Rails credentials.
Example:
# config/initializers/stripe.rb Stripe.api_key = ENV["STRIPE_SECRET_KEY"]
To set an environment variable locally, use:
echo "STRIPE_SECRET_KEY=sk_test_12345" >> .env
10. Document Your Code
Good documentation helps maintain clarity in your application. Use inline comments and tools like YARD to document classes and methods:
# app/models/user.rb # Represents a user in the system # @attr [String] name The name of the user class User < ApplicationRecord validates :name, presence: true end
11. Regularly Refactor
Refactoring is essential for maintaining clean code. Schedule time to refactor as your app grows, and look for areas to simplify or optimize.
Conclusion
By following these best practices, you can create a Ruby on Rails application that is clean, modular, and scalable. While every project is unique, adhering to these guidelines ensures your codebase remains maintainable and efficient, even as your application evolves.