Rails

Configuration

  • Put custom initialisation code in config/initializers. The code in initializers executes on application startup. #
  • Keep initialisation code for each gem in a separate file with the same name as the gem, for example carrierwave.rb, active_admin.rb, etc. #
  • Adjust environment specific settings as required (files under config/environments/) #
  • Mark additional assets for precompilation (if any):
# config/environments/production.rb
# Precompile additional assets (application.js, application.css,
# and all non-JS/CSS are already added)
config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js )
  • Keep configuration that's applicable to all environments in the config/application.rb file. #
  • Create an additional staging environment that closely resembles the production one. #

Routing

  • When you need to add more actions to a RESTful resource (do you really need them at all?) use member and collection routes. #
# bad
get 'subscriptions/:id/unsubscribe'
resources :subscriptions

# good
resources :subscriptions do
  get 'unsubscribe', on: :member
end

# bad
get 'photos/search'
resources :photos

# good
resources :photos do
  get 'search', on: :collection
end
  • If you need to define multiple member/collection routes use the alternative block syntax. #
resources :subscriptions do
  member do
    get 'unsubscribe'
    # more routes
  end
end

resources :photos do
  collection do
    get 'search'
    # more routes
  end
end
  • Use namespaced routes to group related actions. #
namespace :admin do
  # Directs /admin/products/* to Admin::ProductsController
  # (app/controllers/admin/products_controller.rb)
  resources :products
end
  • Never use the legacy wild controller route. This route will make all actions in every controller accessible via GET requests. #
# very bad
match ':controller(/:action(/:id(.:format)))'
  • Don't use match to define any routes unless there is need to map multiple request types among [:get, :post, :patch, :put, :delete] to a single action using :via option. #

Controllers

  • Keep the controllers skinny. They should only retrieve data for the view layer and shouldn't contain any business logic (all the business logic should naturally reside in the model). #
  • Each controller action should (ideally) invoke only one method other than an initial find or new. #
  • Share no more than two instance variables between a controller and a view. #

Models

  • Introduce non-ActiveRecord model classes freely. #
  • Name the models with meaningful (but short) names without abbreviations. #
  • If you need model objects that support ActiveRecord behaviour(like validation) use the ActiveAttr gem. #
class Message
  include ActiveAttr::Model

  attribute :name
  attribute :email
  attribute :content
  attribute :priority

  attr_accessible :name, :email, :content

  validates :name, presence: true
  validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i }
  validates :content, length: { maximum: 500 }
end

For a more complete example refer to the RailsCast on the subject.

ActiveRecord

  • Avoid altering ActiveRecord defaults (table names, primary key, etc) unless you have a very good reason (like a database that's not under your control). #
# bad - don't do this if you can modify the schema
class Transaction < ActiveRecord::Base
  self.table_name = 'order'
  ...
end
  • Group macro-style methods (has_many, validates, etc) in the beginning of the class definition. #
class User < ActiveRecord::Base
  # keep the default scope first (if any)
  default_scope { where(active: true) }

  # constants come up next
  GENDERS = %w(male female)

  # afterwards we put attr related macros
  attr_accessor :formatted_date_of_birth

  attr_accessible :login, :first_name, :last_name, :email, :password

  # followed by association macros
  belongs_to :country

  has_many :authentications, dependent: :destroy

  # and validation macros
  validates :email, presence: true
  validates :username, presence: true
  validates :username, uniqueness: { case_sensitive: false }
  validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ }
  validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true}

  # next we have callbacks
  before_save :cook
  before_save :update_username_lower

  # other macros (like devise's) should be placed after the callbacks

  ...
end
  • Prefer has_many :through to has_and_belongs_to_many. Using has_many :through allows additional attributes and validations on the join model. #
# not so good - using has_and_belongs_to_many
class User < ActiveRecord::Base
  has_and_belongs_to_many :groups
end

class Group < ActiveRecord::Base
  has_and_belongs_to_many :users
end

# preferred way - using has_many :through
class User < ActiveRecord::Base
  has_many :memberships
  has_many :groups, through: :memberships
end

class Membership < ActiveRecord::Base
  belongs_to :user
  belongs_to :group
end

class Group < ActiveRecord::Base
  has_many :memberships
  has_many :users, through: :memberships
end
  • Prefer self[:attribute] over read_attribute(:attribute). #
# bad
def amount
  read_attribute(:amount) * 100
end

# good
def amount
  self[:amount] * 100
end
  • Prefer self[:attribute] = value over write_attribute(:attribute, value). #
# bad
def amount
  write_attribute(:amount, 100)
end

# good
def amount
  self[:amount] = 100
end
# bad
validates_presence_of :email

# good
validates :email, presence: true
  • When a custom validation is used more than once or the validation is some regular expression mapping, create a custom validator file. #
# bad
class Person
  validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
end

# good
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
  end
end

class Person
  validates :email, email: true
end
  • Keep custom validators under app/validators. #
  • Use named scopes freely. #
class User < ActiveRecord::Base
  scope :active, -> { where(active: true) }
  scope :inactive, -> { where(active: false) }

  scope :with_orders, -> { joins(:orders).select('distinct(users.id)') }
end
  • When a named scope defined with a lambda and parameters becomes too complicated, it is preferable to make a class method instead which serves the same purpose of the named scope and returns an ActiveRecord::Relation object. Arguably you can define even simpler scopes like this. #
class User < ActiveRecord::Base
  def self.with_orders
    joins(:orders).select('distinct(users.id)')
  end
end
  • Beware of the behaviour of the update_attribute method. It doesn't run the model validations (unlike update_attributes) and could easily corrupt the model state. #
  • Use user-friendly URLs. Show some descriptive attribute of the model in the URL rather than its id. There is more than one way to achieve this: #

    1. Override the to_param method of the model. This method is used by Rails for constructing a URL to the object. The default implementation returns the id of the record as a String. It could be overridden to include another human-readable attribute. In order to convert this to a URL-friendly value, parameterize should be called on the string. The id of the object needs to be at the beginning so that it can be found by the find method of ActiveRecord.
      class Person
        def to_param
          "#{id} #{name}".parameterize
        end
      end
  • Use find_each to iterate over a collection of AR objects. Looping through a collection of records from the database (using the all method, for example) is very inefficient since it will try to instantiate all the objects at once. In that case, batch processing methods allow you to work with the records in batches, thereby greatly reducing memory consumption. #
# bad
Person.all.each do |person|
  person.do_awesome_stuff
end

Person.where('age > 21').each do |person|
  person.party_all_night!
end

# good
Person.find_each do |person|
  person.do_awesome_stuff
end

Person.where('age > 21').find_each do |person|
  person.party_all_night!
end
# bad (roles will be deleted automatically even if super_admin? is true)
has_many :roles, dependent: :destroy

before_destroy :ensure_deletable

def ensure_deletable
  fail "Cannot delete super admin." if super_admin?
end

# good
has_many :roles, dependent: :destroy

before_destroy :ensure_deletable, prepend: true

def ensure_deletable
  fail "Cannot delete super admin." if super_admin?
end

Migrations

  • Keep the schema.rb (or structure.sql) under version control. #
  • Use rake db:schema:load instead of rake db:migrate to initialise an empty database. #
  • Enforce default values in the migrations themselves instead of in the application layer. #
# bad - application enforced default value
def amount
  self[:amount] or 0
end

While enforcing table defaults only in Rails is suggested by many Rails developers, it's an extremely brittle approach that leaves your data vulnerable to many application bugs. And you'll have to consider the fact that most non-trivial apps share a database with other applications, so imposing data integrity from the Rails app is impossible.

  • Enforce foreign-key constraints. While ActiveRecord does not support them natively, there some great third-party gems like schema_plus and foreigner. #
  • When writing constructive migrations (adding tables or columns), use the change method instead of up and down methods. #
# the old way
class AddNameToPeople < ActiveRecord::Migration
  def up
    add_column :people, :name, :string
  end

  def down
    remove_column :people, :name
  end
end

# the new preferred way
class AddNameToPeople < ActiveRecord::Migration
  def change
    add_column :people, :name, :string
  end
end
  • Don't use model classes in migrations. The model classes are constantly evolving and at some point in the future migrations that used to work might stop, because of changes in the models used. #

Views

  • Never call the model layer directly from a view. #
  • Never make complex formatting in the views, export the formatting to a method in the view helper or the model. #
  • Mitigate code duplication by using partial templates and layouts. #

Internationalisation

  • No strings or other locale specific settings should be used in the views, models and controllers. These texts should be moved to the locale files in the config/locales directory. #
  • When the labels of an ActiveRecord model need to be translated, use the activerecord scope: #

en: activerecord: models: user: Member attributes: user: name: 'Full name'

Then User.model_name.human will return "Member" and User.human_attribute_name("name") will return "Full name". These translations of the attributes will be used as labels in the views.

  • Separate the texts used in the views from translations of ActiveRecord attributes. Place the locale files for the models in a folder models and the texts used in the views in folder views. #
  • When organisation of the locale files is done with additional directories, these directories must be described in the application.rb file in order to be loaded.
# config/application.rb
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
  • Place the shared localisation options, such as date or currency formats, in files under the root of the locales directory. #
  • Use the short form of the I18n methods: I18n.t instead of I18n.translate and I18n.l instead of I18n.localize. #
  • Use "lazy" lookup for the texts used in views. Let's say we have the following structure: #

en: users: show: title: 'User details page'

The value for users.show.title can be looked up in the template app/views/users/show.html.haml like this:

= t '.title'
  • Use the dot-separated keys in the controllers and models instead of specifying the :scope option. The dot-separated call is easier to read and trace the hierarchy. #
# use this call
I18n.t 'activerecord.errors.messages.record_invalid'

# instead of this
I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages]
  • More detailed information about the Rails i18n can be found in the Rails Guides #

Assets

Use the assets pipeline to leverage organisation within your application.

  • Reserve app/assets for custom stylesheets, JavaScripts, or images. #
  • Use lib/assets for your own libraries that don’t really fit into the scope of the application.
  • Third party code such as jQuery or bootstrap should be placed in vendor/assets.
  • When possible, use gemified versions of assets (e.g. jquery-rails, jquery-ui-rails, bootstrap-sass, zurb-foundation).

Mailers

  • Name the mailers SomethingMailer. Without the Mailer suffix it isn't immediately apparent what's a mailer and which views are related to the mailer. #
  • Provide both HTML and plain-text view templates. #
  • Enable errors raised on failed mail delivery in your development environment. The errors are disabled by default. #
# config/environments/development.rb

config.action_mailer.raise_delivery_errors = true
  • Use a local SMTP server like Mailcatcher in the development environment. #
# config/environments/development.rb

config.action_mailer.smtp_settings = {
  address: 'localhost',
  port: 1025,
  # more settings
}
  • Provide default settings for the host name. #
# config/environments/development.rb
config.action_mailer.default_url_options = { host: "#{local_ip}:3000" }

# config/environments/production.rb
config.action_mailer.default_url_options = { host: 'your_site.com' }

# in your mailer class
default_url_options[:host] = 'your_site.com'
  • If you need to use a link to your site in an email, always use the _url, not _path methods. The _url methods include the host name and the _path methods don't. #
# bad
You can always find more info about this course
= link_to 'here', course_path(@course)

# good
You can always find more info about this course
= link_to 'here', course_url(@course)
  • Format the from and to addresses properly. Use the following format: #
# in your mailer class
default from: 'Your Name <info@your_site.com>'
  • Make sure that the e-mail delivery method for your test environment is set to test: #
# config/environments/test.rb

config.action_mailer.delivery_method = :test
  • The delivery method for development and production should be smtp: #
# config/environments/development.rb, config/environments/production.rb

config.action_mailer.delivery_method = :smtp
  • When sending html emails all styles should be inline, as some mail clients have problems with external styles. This however makes them harder to maintain and leads to code duplication. There are two similar gems that transform the styles and put them in the corresponding html tags: premailer-rails and roadie. #
  • Sending emails while generating page response should be avoided. It causes delays in loading of the page and request can timeout if multiple email are sent. To overcome this emails can be sent in background process. #

Bundler

  • Put gems used only for development or testing in the appropriate group in the Gemfile. #
  • Use only established gems in your projects. If you're contemplating on including some little-known gem you should do a careful review of its source code first. #
  • Do not remove the Gemfile.lock from version control. This is not some randomly generated file! It makes sure that all of your team members get the same gem versions when they do a bundle install. #

References

Further Reading