Ruby on Rails πŸš‡

Hero image for Ruby on Rails πŸš‡

All Ruby conventions apply to Ruby on Rails but working with a framework adds another superset of conventions to follow.

PORO stands for Plain Old Ruby Objects and is used to differentiate between objects inheriting functionality from Ruby-on-Rails and those who do not (the latter are POROs).

Naming

  • Suffix each project name with -web or -api depending on the project type.
# Bad
rails new project-name
rails new project-name --api

# Good
rails new project-name-web
rails new project-name-api --api

Use the team template for all Ruby on Rails projects.

  • Use snake_case for both database tables and columns. But use plural for database tables and singular for column names.
+---------------------------+
| campaign_locations        |
+-------------+-------------+
| id          | ID          |
| name        | STRING      |
| location_id | FOREIGN KEY |
| updated_at  | DATETIME    |
+-------------+-------------+
  • Combine table names to name join tables choosing to pluralize based on the intent of the table.
# Given these two models
class Campaign < ApplicationRecord
  has_many :influencers, inverse_of: :campaign
end

class Influencer < ApplicationRecord
  has_many :campaigns, inverse_of: :influencer
end

# With the resulting join table

# Bad
class Campaign < ApplicationRecord
  has_many :influencers, through: :campaigns_influencers
  has_many :campaigns_influencers, inverse_of: :campaign
end

class Influencer < ApplicationRecord
  has_many :campaigns, through: :campaigns_influencers
end

# Good
class Campaign < ApplicationRecord
  has_many :influencers, through: :campaign_influencers
  has_many :campaign_influencers, inverse_of: :campaign
end

class Influencer < ApplicationRecord
  has_many :campaigns, through: :campaign_influencers
end
  • Use predicate-like name for boolean database columns.
# Bad
add_column :users, :enabled, :boolean, null: false, index: true

# Good
add_column :users, :is_enabled, :boolean, null: false, index: true
  • Use kebab-case for naming assets files.
# Bad
app/assets/images/user_avatar.svg
app/assets/stylesheets/components/_card_pricing.scss
public/assets/images/error_404.svg

# Good
app/assets/images/user-avatar.svg
app/assets/stylesheets/components/_card-pricing.scss
public/assets/images/error-404.svg
  • When adding β€œnon-conventional” directories in /app, use the parent directory singular name as a suffix for filenames inside that directory.
β”œβ”€β”€ forms
β”‚Β Β  └── create_otp_form.rb
β”œβ”€β”€ ...
β”œβ”€β”€ queries
β”‚Β Β  └── package_query.rb
β”œβ”€β”€ ...
└── services
β”‚Β Β  └── create_user_service.rb
β”œβ”€β”€ ...
  • Use the kebab-case project name as a namespace and prefix to name modules and engines.
# Bad
engines/campaign/
lib/errors/

# Good
engines/project-name_campaign/
lib/project-name/errors/

Syntax

  • Use Time.current instead of Time.now to use the timezone configured in the Rails environment.

Configuration

  • Use fetch to access environment variables.
# Bad
config.action_mailer.default_url_options = {
  host: ENV['DEFAULT_HOST'],
  port: ENV['DEFAULT_PORT']
}

# Good
config.action_mailer.default_url_options = {
  host: ENV.fetch('DEFAULT_HOST'),
  port: ENV.fetch('DEFAULT_PORT')
}
  • Provide a default value for non-sensitive variable when possible (i.e. not secret credentials).
ENV.fetch('NON_EXISTING_NON-SENSITIVE_VARIABLE', 'default value')
  • Prefer using an initializer to store environment variables as constants. Name this file as project-name.rb:
# in config/initializers/project-name.rb
BASIC_AUTHENTICATION_USERNAME = ENV.fetch('BASIC_AUTHENTICATION_USERNAME')
BASIC_AUTHENTICATION_PASSWORD = ENV.fetch('BASIC_AUTHENTICATION_PASSWORD')

# in app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  http_basic_authenticate_with name: BASIC_AUTHENTICATION_USERNAME,
                               password: BASIC_AUTHENTICATION_PASSWORD
end

Since this file is loaded on the application boot, it will fail if an environment variable is missing i.e. fast-fail strategy.

Models

  • Code in models must follow the same structure as Ruby classes with additional blocks.
class SomeModel < ApplicationRecord
  # extend and include custom modules
  extend SomeModule
  include SomeModule

  # refinements
  using ArrayExtensions

  # third party macros
  acts_as_paranoid

  # constants
  SOME_CONSTANT = '20'.freeze

  # attributes, enums and accessors
  attr_accessor :name, :last_name

  enum status: { inactive: 0, active: 1, blacklisted: 2 }, _suffix: true

  store_accessor :social_accounts, %i[facebook linkedin twitter]

  # relationships
  has_many :posts, inverse_of: :some_model
  belongs_to :organization, inverse_of: :some_model

  # delegations
  delegate :full_name, to: :model_decorator

  # validations
  validates :name, presence: true
  validates_with CustomValidatorClass
  validate :custom_validator_method

  # scopes
  default_scope { order(first_name: :asc) }
  scope :with_relationship, -> { include(:relationship) }

  # callbacks
  after_create_commit :notify_reviewer

  # public class methods
  class << self
    def some_method
    end
  end

  # initialization
  def initialize
  end

  # public instance methods
  def some_public_method
  end

  def model_decorator
    @model_decorator ||= ModelDecorator.new(self)
  end

  # protected methods
  protected

  def some_protected_method
  end

  # private methods
  private

  def notify_reviewer
  end
end

When dealing with PORO models, then the default structure for Ruby classes applies.

  • Define explicitly enum keys to avoid both data and implementation inconsistencies. The latter is often caused when non-positive values are needed.
# Bad
enum status: [:inactive, :active, :blacklisted]

# Good
enum status: { inactive: 0, active: 1, blacklisted: 2 }
enum status: { inactive: -1, active: 0, blacklisted: 1 }
  • Use inverse_of option when defining associations to prevent avoidable SQL queries and keep models in sync.
# Bad
class Author < ApplicationRecord
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :author
end

# Good
class Author < ApplicationRecord
  has_many :books, inverse_of: :author
end

class Book < ApplicationRecord
  belongs_to :author, inverse_of: :books
end
  • Use validates with options instead of convenience methods such as validates_option_name_of.
# Bad
validates_presence_of :name

# Good
validates :name, presence: true
  • Do not add presentation logic in models, use Decorators instead.

Non-ActiveRecord Models

Not every model is backed by a database. When to use Concerns for appearance, when to encapsulate logic in plain-old Ruby objects. – @dhh

  • Use Single Table Inheritance (STI) to decouple models from database tables and to map better domain concerns to separate objects.

    In its simplest implementation, STI requires a type column and each sub-model to inherit from the same parent class:

      # Bad
      class Transaction < ApplicationRecord
        TRANSACTION_TYPES = %w[Deposit Withdrawal].freeze
    
        validates :type, inclusion: { in: TRANSACTION_TYPES }
    
        def self.create_deposit(attributes = {})
          transaction_type = 'Deposit'
          #...
        end
    
        def self.create_withdrawal(attributes = {})
          transaction_type = 'Withdrawal'
          #...
        end
      end
    
      # Good
      class Transaction < ApplicationRecord
      end
    
      class Deposit < Transaction
      end
    
      class Withdrawal < Transaction
      end
    

    In more complex implementations – where a type column is not possible – STI can be implemented manually using default_scope:

      class Transaction < ApplicationRecord
        enum transaction_type: { deposit: 0, withdrawal: 1 }
      end
    
      class Deposit < Transaction
    
        default_scope { where(transaction_type: :deposit) }
    
        def initialize(attributes = {})
          super(attributes) do
            transaction_type = :deposit
          end
        end
      end
    
  • Use value objects to extract domain concerns.

class DateRange
  DATE_FORMAT = '%d-%B-%Y'.freeze

  attr_reader :start_date, :end_date

  def initialize(start_date, end_date)
    @start_date = start_date
    @end_date = end_date
  end

  def include_date?(date)
    date >= start_date && date <= end_date
  end

  def to_s
    "from #{start_date.strftime(DATE_FORMAT)} to #{end_date.strftime(DATE_FORMAT)}"
  end
end

Which can be used in an ActiveRecord model:

class Event < ApplicationRecord
  def date_range
    DateRange.new(start_date, end_date)
  end

  def date_range=(date_range)
    self.start_date = date_range.start_date
    self.end_date = date_range.end_date
  end
end
  • Place non-ActiveRecord models – such as value objects but not only – in the directory /models along with ActiveRecord-backed models.

Optimizations

Optimize relationships to avoid n+1 queries:

  • Eager load relationships if needed:

      class Booking < ApplicationRecord
        belongs_to :photographer_package
    
        scope :with_photographer_package, -> { includes(:photographer_package) }
      end
    

    In most cases, it’s better to let Rails optimize the pre-fetching logic using includes but in some cases preload or eager_load must be used directly.

  • Use the gem Bullet to detect n+1 queries.

Controllers

  • Code in controllers must follow the same structure as Ruby classes with specific blocks.
class SomeController < ApplicationController
  # extend and include
  extend SomeConcern
  include AnotherConcern

  # constants
  SOME_CONSTANT = '20'.freeze

  # callbacks
  before_action :authenticate_user!
  before_action :set_resource, only: :index
  before_action :authorize_resource!, only: :index
  before_action :set_requested_resource, only: :show
  before_action :authorize_requested_resource!, only: :show

  # public instance methods
  def index
  end

  def show
  end

  def new
  end

  def edit
  end

  def create
  end

  def update
  end

  def destroy
  end

  # protected methods
  protected

  def current_user
    @current_user = super || NullUser.new
  end

  # private methods
  private

  def set_resource
  end

  def authorize_resource!
  end

  def set_requested_resource
  end

  def authorize_requested_resource!
  end
end
  • Use strong params to whitelist the list of parameters sent to the controller.
# Bad
request.parameters[:user][:name]

# Good
params.require(:user).permit(:name)

Views

  • Use local variables instead of instance variables to pass data to views.
# Bad
class UsersController
  def show
    @user = User.find(params[:id])
    @user_presenter = UserPresenter.new(@user)

    render new
  end
end

# Good
class UsersController
  def show
    user = User.find(params[:id])
    user_presenter = UserPresenter.new(@user)

    render new, locals: {
      user: user,
      user_presenter: user_presenter
    }
  end
end
  • Do not add presentation logic in views, use Presenters instead.

Project Structure

The structure is consistent with the conventional and built-in structure of Ruby on Rails – which is documented in the official Rails Guide – with the addition of unconventional directories (highlighted with *):

app/
β”œβ”€β”€ assets/
β”œβ”€β”€ channels/
β”œβ”€β”€ controllers/
β”œβ”€β”€ decorators/ *
β”œβ”€β”€ forms/ *
β”œβ”€β”€ helpers/
β”œβ”€β”€ javascript/
β”œβ”€β”€ jobs/
β”œβ”€β”€ mailers/
β”œβ”€β”€ models/
β”œβ”€β”€ policies/ *
β”œβ”€β”€ presenters/ *
β”œβ”€β”€ queries/ *
β”œβ”€β”€ serializers/ *
β”œβ”€β”€ services/ *
β”œβ”€β”€ validators/ *
β”œβ”€β”€ views/
bin/
config/
engines/ *
public/
storage/
vendor/

Decorators

  • Decorators are POROs abstracting view methods from models.
# Bad
class User < ApplicationRecord
  def full_name
    "#{first_name} #{last_name}"
  end
end

# Good
class User < ApplicationRecord
  delegate :full_name, to: :user_decorator

  def user_decorator
     @user_decorator ||= UserDecorator.new(self)
  end
end

class UserDecorator
  attr_reader :user

  def initialize(user)
    @user = user
  end

  def full_name
    "#{user.first_name} #{user.last_name}"
  end
end

# Better
class User < ApplicationRecord
  delegate :full_name, to: :user_decorator

  def user_decorator
     @user_decorator ||= UserDecorator.new(self)
  end
end

class UserDecorator < SimpleDelegator
  def full_name
    "#{first_name} #{last_name}"
  end
end
  • SimpleDelegator provides the means to delegate all supported method calls to the object passed into the constructor.

Decorators logic is meant to be used solely in backend-related areas e.g. other models, asynchronous jobs, form objects, services… When in need to format data for a view, use a presenter instead.

Forms

  • Forms are POROs abstracting complex logic from controllers.
class CampaignEnrollingForm
  include ActiveModel::Model

  attr_accessor :user

  def initialize(user)
    @user = user
  end

  def save(params)
    ActiveRecord::Base.transaction do
      user.assign_attributes(params)
      user.charge!
      user.enroll!

      raise ActiveRecord::Rollback unless user.save
    end

    promote_errors(user.errors)
    errors.empty?
  end

  private

  def promote_errors(child_errors)
    child_errors.each do |attribute, message|
      errors.add(attribute, message)
    end
  end
end
  • Include ActiveModel::Model to inherit attributes assignments and validations.

  • Use CRUD-like operation to name public methods providing an ActiveRecord-like interface e.g. save, update or destroy.

Read a detailed overview of Form Objects on our blog πŸš€

Policies

  • Policies are POROs handling authorization logic.
class CampaignPolicy < ApplicationPolicy
  def edit?
    user.completed_onboarding?
  end

  def update?
    user.completed_onboarding?
  end

  def manage?
    false
  end

  alias index? manage?
  alias show? manage?
  alias new? manage?
  alias create? manage?
  alias destroy? manage?
end
  • Prefer using the gem pundit to implement authorization logic.

Presenters

Presenters are POROs abstracting view methods from models, controllers and views.

# Bad
# app/views/users/show.html.slim
span.onboarding__message
  - if user.onboarded?
    = t('users.onboarding.welcome')
  - else
    = t('users.onboarding.welcome_back')

# Good
# app/views/users/show.html.slim
span.onboarding__message
  = user_presenter.onboarding_message

# app/presenters/user_presenter.rb
class UserPresenter
  attr_reader :user

  def initialize(user)
    @user = user
  end

  def onboarding_message
    return I18n.t('users.onboarding.welcome') if user.onboarded?

    I18n.t('users.onboarding.welcome_back')
  end
end

Presenters logic is meant to be used solely in views-related areas e.g. web views, mailers, printed views. When in need to format data for backend purposes, use a decorator instead.

Queries

  • Queries are POROs abstracting complex database fetching, sorting and ordering logic.
class UsersQuery
  attr_reader :users, :filters

  def initialize(users = User.all, filters = {})
    @users = users
    @filters = filters
  end

  def call
    @users = date_filtered_users if filter_by_date.present?
    @users = users.includes(:location)
  end

  private

  def date_filtered_users
    users.where(created_at: filter_start_date..filter_end_date)
  end

  def filter_by_date
    filters[:start_date] || filters[:end_date]
  end

  def filter_start_date
    filters[:start_date].to_date.beginning_of_day
  rescue ArgumentError, NoMethodError
    start_of_time.beginning_of_day
  end

  def filter_end_date
    filters[:end_date].to_date.end_of_day
  rescue ArgumentError, NoMethodError
    Time.current.end_of_day
  end

  def start_of_time
    User.order(:created_at).limit(1).first.created_at
  end
end
  • For complex data computation, use database views and materialized views. Prefer using the gem scenic for an efficient integration with ActiveRecord::Migration.

Serializers

  • Serializers are POROs handling API responses rendering.
class CampaignSerializer
  include FastJsonapi::ObjectSerializer

  attributes :name, :year

  has_many :users
  belongs_to :account
end
  • Prefer using the gem fast_jsonapi to implement Ruby Objects serialization.

Services

Services are POROs handling business logic and the connections between the domain objects.

class CreateChargeService
  def initialize(amount:, token:, **options)
    @amount = amount
    @token = token
    @customer_id = options[:customer_id]
  end

  def call
    raise Nimble::Errors::PaymentGatewayError, charge.failure_message.capitalize unless charge.paid

    charge
  rescue PaymentGateway::Error => e
    raise Nimble::Errors::PaymentGatewayError, e
  end

  private

  attr_reader :amount, :token, :customer_id

  def charge
    @charge ||= PaymentGateway::Charge.create(amount: charge_amount,
                                              currency: Currency.default.iso_code.downcase,
                                              card: token,
                                              customer: customer_id)
  end
end

Validators

Validators are ActiveModel-based validator classes extending ActiveModel validations.

class FutureDateValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors.add(attribute, (options[:message] || :future_date)) unless in_future?(value)
  end

  private

  def in_future?(date)
    date.present? && Time.zone.parse(date.to_s).to_datetime > Time.zone.today
  end
end

Engines

Engines are built-in into Ruby on Rails; even the Rails::Application inherit from Rails::Engine. We use engines to split large monolithic applications into sub-sets of mini-applications. The main benefits:

  • Separation of concerns with clear boundary definitions between domain objects. Even asset manifest files (CSS and JS) are split resulting into better performance and easier maintenance with direct dependencies.
  • Force developers to architect/design the domain objects instead of grouping everything into one main app folder.
  • Easier management of functionality β€œgrowth” over time. The addition of a new feature becomes a matter of deciding in which engine it fits or in rare cases if it requires a new engine.
  • Initial built-in horizontal scalability as each engine can be mounted independently. For instance, it’s possible to run 2 instances for a specific engine (e.g. checkout) but only 1 instance for another engine (e.g. admin) by mounting selectively engines on each instance.
Rails.application.routes.draw do
  mount ProjectNameAdmin::Engine, at: '/', if: ENGINE_ADMIN_ENABLED
  mount ProjectNameCheckout::Engine, at: '/', if: ENGINE_CHECKOUT_ENABLED
end

Engines are not a silver bullet and comes with additional initial architecture and setup costs. So engine-based architecture should be preferred for medium to large applications. For small applications (< 3-5 models and controllers), the conventional architecture – placing all code in app/ – can be sufficient.

  • Use the kebab-case project name as a namespace for all engines for modularization purposes.
// Bad
engines/
β”œβ”€β”€ admin/
β”œβ”€β”€ checkout/
β”œβ”€β”€ settings/
β”œβ”€β”€ website/

// Good
engines/
β”œβ”€β”€ project-name_admin/
β”œβ”€β”€ project-name_checkout/
β”œβ”€β”€ project-name_setting/
β”œβ”€β”€ project-name_website/

Make sure to configure the namespace in the engine:

module ProjectNameCheckout
  class Engine < ::Rails::Engine
    isolate_namespace ProjetNameCheckout
  end
end
  • Use singular to name engines.
// Bad
engines/
β”œβ”€β”€ project-name_campaigns/
β”œβ”€β”€ project-name_profiles/
β”œβ”€β”€ project-name_settings/

// Good
engines/
β”œβ”€β”€ project-name_campaign/
β”œβ”€β”€ project-name_profile/
β”œβ”€β”€ project-name_setting/
  • Store shared domain objects – typically almost all models and policies – and functionality – typically stylesheets and JavaScript – into the main app directory. /app becomes the core application. Each engine has its own specific models, policies and assets.
app/
β”œβ”€β”€ assets/
β”‚Β Β  └── stylesheets/
β”‚Β Β   Β Β  └── core.scss
β”œβ”€β”€ controllers/
β”œβ”€β”€ helpers/
β”‚Β Β  └── svg_helper.rb
β”œβ”€β”€ javascript/
β”‚Β Β  └── core.js
β”œβ”€β”€ jobs/
β”œβ”€β”€ mailers/
β”œβ”€β”€ models/
β”‚Β Β  β”œβ”€β”€ campaign.rb
β”‚Β Β  β”œβ”€β”€ ...
β”‚Β Β  └── user.rb
β”œβ”€β”€ policies/ *
β”‚Β Β  β”œβ”€β”€ campaign_policy.rb
β”‚Β Β  β”œβ”€β”€ ...
β”‚Β Β  └── user_policy.rb
bin/
config/
engines/
β”œβ”€β”€ project-name_admin/
β”œβ”€β”€ project-name_checkout/
β”œβ”€β”€ project-name_settings/
β”œβ”€β”€ project-name_website/
public/
storage/
vendor/
  • Define only shared gems in the root Gemfile. If a gem is used in only one engine, it must be defined in the engine’s gemspec file.
# In Gemfile
source 'https://rubygems.org'

ruby '2.5.3'

# Backend
gem 'rails', '6.0.0' # Latest stable.
gem 'puma' # Use Puma as the app server.

# Authorizations
gem 'pundit' # Minimal authorization through OO design and pure Ruby classes.


# In engines/project-name_auth/project-name_auth.gemspec
Gem::Specification.new do |s|
  # ...

  s.add_dependency 'devise'

  s.add_dependency 'omniauth'
  s.add_dependency 'omniauth-facebook'
  s.add_dependency 'omniauth-twitter'
end
  • Define all engines in the root Gemfile and into a group named engines.
# Bad
gem 'project-name_admin', path: 'engines/project-name_admin'
gem 'project-name_checkout', path: 'engines/project-name_checkout'

# Good
group :engines do
  gem 'project-name_admin', path: 'engines/project-name_admin'
  gem 'project-name_checkout', path: 'engines/project-name_checkout'
end

This group can easily be required in application.rb:

require_relative 'boot'

require 'rails/all'

Bundler.require(:engines, *Rails.groups)
  • If a middleware is used in only one engine, it must be defined in the engine’s engine file with middleware.use instead of app.middleware.use
# Bad
module ProjectNameAdmin
  class Engine < ::Rails::Engine
    isolate_namespace ProjetNameAdmin

    initializer 'project-name_admin.add_middleware' do |app|
      app.middleware.use CustomMiddleware
    end
  end
end

# Good
module ProjectNameAdmin
  class Engine < ::Rails::Engine
    isolate_namespace ProjetNameAdmin

    initializer 'project-name_admin.add_middleware' do
      middleware.use CustomMiddleware
    end
  end
end
  • Prefer placing all specs in the root /spec directory instead inside of the engine. The main rationale is that all specs are run together and share the same setup and configuration (/spec/support).
// Bad
engines/
β”œβ”€β”€ project-name_admin/
β”‚Β Β  └── spec/ *
β”œβ”€β”€ project-name_checkout/
β”‚Β Β  └── spec/ *

// Good
engines/
β”œβ”€β”€ project-name_admin/
β”œβ”€β”€ project-name_checkout/
spec/
β”œβ”€β”€ controllers/
β”‚Β Β  └── project-name_admin/ *
β”‚Β Β   Β Β  └── *_spec.rb
β”‚Β Β  └── project-name_checkout/ *
β”‚Β Β   Β Β  └── *_spec.rb

Database

Relational Database

  • Prefer using PostgreSQL for relational data storage.

  • Use database indexes on foreign keys and boolean columns for faster queries.

add_column :users, :location_id, :integer, foreign_key: true, index: true
add_column :users, :is_enabled, :boolean, null: false, default: false, index: true
  • Use database constraints such as null and default along with model validations.
# Given this model
class User < ApplicationRecord
  validates :name, presence: true
  validates :username, presence: true, uniqueness: true
  validates :encrypted_password, presence: true
end

# Bad
create_table 'users', force: :cascade do |t|
  t.name :string
  t.username :string, index: true
  t.encrypted_password :string

  t.timestamps
end

# Good
create_table 'users', force: :cascade do |t|
  t.name :string, null: false
  t.username :string, null: false, index: true
  t.encrypted_password :string, null: false

  t.timestamps

  t.index :username, unique: true
end
# Bad
add_column :users, :is_enabled, :boolean, index: true

# Good
add_column :users, :is_enabled, :boolean, null: false, default: false, index: true
  • Use bigint or uuid data type column for storing primary IDs.

  • Prefer citext data type column for storing case insensitive data such as emails.

  • Prefer jsonb data type column for storing object-like data over hstore and json. Settings-like or configuration-like data are good candidates to be stored as jsonb.

# schema.rb
create_table "campaigns", force: :cascade do |t|
  t.string "name", null: false
  t.decimal "budget_amount", precision: 19, scale: 2, default: "0.0"
  t.jsonb "platforms"
  t.bigint "user_id", null: false, index: true
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

# app/models/campaigns
class Campaign < ApplicationRecord
  store_accessor :platforms, %i[facebook linkedin twitter]

  belongs_to :advertiser, inverse_of: :campaigns, foreign_key: 'user_id'
end
  • Always soft-delete relational data for integrity and audit purposes. Gems such as paranoia and discard provide ready-to-use solutions.

  • Name the migration files more elaborately. For table migration, Suppose you add/drop any column to/from a table then, keep both column_name and table_name in migration name. The name of the migration name will like DropSurnameFromUsers. Or, for multiple migration together, just concat the column names and table names in the class name. So the general template is [ChangeMade]+To/From+[TableNamePlural]

# Bad
class DropSurnameColumn < ActiveRecord::Migration[6.0]
  def change
    remove_column :users, :surname
  end
end

# Good
class DropSurnameFromUsers < ActiveRecord::Migration[6.0]
  def change
    remove_column :users, :surname
  end
end

class DropSurnameNicknameFromUsers < ActiveRecord::Migration[6.0]
  def change
    remove_column :users, :surname
    remove_column :users, :nickname
  end
end
  • For data migration, use meaningful name also.
# Bad
class UpdateWebsite < ActiveRecord::Migration[6.0]
  def change
    # your changes to DB here
  end
end

# Good
class UpdateWebsiteColumnOfUsers < ActiveRecord::Migration[6.0]
  def change
    # your changes to DB here
  end
end

Non-relational Database

  • Prefer using Redis for key/value data storage.

  • Prefer using ElasticSearch for document-oriented data storage.

Security

  • Use prepared statements for database operations requiring user inputs to prevent SQL injections.
# Bad
User.where("last_seen_at > #{params[:start_datetime]}")

# Good
User.where('last_seen_at > ?', params[:start_datetime])
  • Use hash-based parameters for database operations requiring constants.
# Bad
User.where('status = ?', :new)

# Good
User.where(status: :active)
  • Beware of initialing objects directly based on user input. Always add a validation layer or even prefer using factories.
class FooForm; end
class BarForm; end

# Bad
form_klass = "#{params[:kind].camelize}Form".constantize
form_klass.new.submit(params)

# Good
klasses = {
  'foo' => FooForm,
  'bar' => BarForm
}

klass = klasses[params[:kind]]
if klass
  klass.new.submit(params)
end

# Best
class FormFactory
  def self.build(type, *args)
    case type
    when :foo
      FooForm.new(*args)
    when :bar
      BarForm.new(*args)
    end
  end
end

Documentation

Use YARD docblock to document public methods.

module PaymentGateway
  module V1
    class Account < Resources::BaseResource
      # @param [String] access_token
      # @param [Hash] args
      # @option args [Integer] :version
      # @return [Object]
      def retrieve_identification(access_token, args)
        #...
      end
    end
  end
end