Role models are important.
-- Officer Alex J. Murphy / RoboCop
The goal of this guide is to present a set of best practices and style prescriptions for Ruby on Rails 4 development. It's a complementary guide to the already existing community-driven Ruby coding style guide.
Some of the advice here is applicable only to Rails 4.0+.
You can generate a PDF or an HTML copy of this guide using Transmuter.
Translations of the guide are available in the following languages:
This Rails style guide recommends best practices so that real-world Rails programmers can write code that can be maintained by other real-world Rails programmers. A style guide that reflects real-world usage gets used, and a style guide that holds to an ideal that has been rejected by the people it is supposed to help risks not getting used at all – no matter how good it is.
The guide is separated into several sections of related rules. I've tried to add the rationale behind the rules (if it's omitted I've assumed it's pretty obvious).
I didn't come up with all the rules out of nowhere - they are mostly based on my extensive career as a professional software engineer, feedback and suggestions from members of the Rails community and various highly regarded Rails programming resources.
- Configuration
- Routing
- Controllers
- Models
- Migrations
- Views
- Internationalization
- Assets
- Mailers
- Time
- Bundler
- Flawed Gems
- Managing processes
-
Put custom initialization code in
config/initializers
. The code in initializers executes on application startup.
-
Keep initialization code for each gem in a separate file with the same name
as the gem, for example
carrierwave.rb
,active_admin.rb
, etc.
-
Adjust accordingly the settings for development, test and production
environment (in the corresponding 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.
Since Rails 4.2 YAML configuration files can be easily loaded with the new config_for
method:
Rails::Application.config_for(:yaml_file)
-
When you need to add more actions to a RESTful resource (do you really need
them at all?) use
member
andcollection
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
resources :subscriptions do
member do
get 'unsubscribe'
# more routes
end
end
resources :photos do
collection do
get 'search'
# more routes
end
end
class Post < ActiveRecord::Base
has_many :comments
end
class Comments < ActiveRecord::Base
belongs_to :post
end
# routes.rb
resources :posts do
resources :comments
end
-
If you need to nest routes more than 1 level deep then use the
shallow: true
option. This will save user from long urlsposts/1/comments/5/versions/7/edit
and you from long url helpersedit_post_comment_version
.resources :posts, shallow: true do resources :comments do resources :versions end end
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.
- 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).
- If you need model objects that support ActiveRecord behavior (like validation) without the ActiveRecord database functionality 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.
- 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
class User < ActiveRecord::Base
# keep the default scope first (if any)
default_scope { where(active: true) }
# constants come up next
COLORS = %w(red green blue)
# 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
tohas_and_belongs_to_many
. Usinghas_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
# prefered 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
# bad
def amount
read_attribute(:amount) * 100
end
# good
def amount
self[:amount] * 100
end
# bad
def amount
write_attribute(:amount, 100)
end
# good
def amount
self[:amount] = 100
end
- Always use the new "sexy" validations.
# bad
validates_presence_of :email
validates_length_of :email, maximum: 100
# good
validates :email, presence: true, length: { maximum: 100 }
- 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
- Consider extracting custom validators to a shared gem if you're maintaining several related apps or the validators are generic enough.
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
Note that this style of scoping cannot be chained in the same way as named scopes. For instance:
# unchainable
class User < ActiveRecord::Base
def User.old
where('age > ?', 80)
end
def User.heavy
where('weight > ?', 200)
end
end
In this style both old
and heavy
work individually, but you cannot call User.old.heavy
, to chain these scopes use:
# chainable
class User < ActiveRecord::Base
scope :old, -> { where('age > 60') }
scope :heavy, -> { where('weight > 200') }
end
-
Beware of the behavior of the
update_attribute
method. It doesn't run the model validations (unlikeupdate_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:
-
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 theid
of the record as a String. It could be overridden to include another human-readable attribute.class Person def to_param "#{id} #{name}".parameterize end end
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.
-
Use the
friendly_id
gem. It allows creation of human-readable URLs by using some descriptive attribute of the model instead of itsid
.class Person extend FriendlyId friendly_id :name, use: :slugged end
Check the gem documentation for more information about its usage.
-
Use
find_each
to iterate over a collection of AR objects. Looping through a collection of records from the database (using theall
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
-
Since Rails creates callbacks for dependent
associations, always call
before_destroy
callbacks that perform validation withprepend: true
.
# 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
- Avoid string interpolation in queries, as it will make your code susceptible to SQL injection attacks.
# bad - param will be interpolated unescaped
Client.where("orders_count = #{params[:orders]}")
# good - param will be properly escaped
Client.where('orders_count = ?', params[:orders])
- Consider using named placeholders instead of positional placeholders when you have more than 1 placeholder in your query.
# okish
Client.where(
'created_at >= ? AND created_at <= ?',
params[:start_date], params[:end_date]
)
# good
Client.where(
'created_at >= :start_date AND created_at <= :end_date',
start_date: params[:start_date], end_date: params[:end_date]
)
# bad
User.where(id: id).take
# good
User.find(id)
# bad
User.where(first_name: 'Bruce', last_name: 'Wayne').first
# good
User.find_by(first_name: 'Bruce', last_name: 'Wayne')
# bad - loads all the records at once
# This is very inefficient when the users table has thousands of rows.
User.all.each do |user|
NewsMailer.weekly(user).deliver_now
end
# good - records are retrieved in batches
User.find_each do |user|
NewsMailer.weekly(user).deliver_now
end
# bad
User.where("id != ?", id)
# good
User.where.not(id: id)
-
When specifying an explicit query in a method such as
find_by_sql
, use heredocs withsquish
. This allows you to legibly format the SQL with line breaks and indentations, while supporting syntax highlighting in many tools (including GitHub, Atom, and RubyMine).
User.find_by_sql(<<SQL.squish)
SELECT
users.id, accounts.plan
FROM
users
INNER JOIN
accounts
ON
accounts.user_id = users.id
# further complexities...
SQL
String#squish
removes the indentation and newline characters so that your server
log shows a fluid string of SQL rather than something like this:
SELECT\n users.id, accounts.plan\n FROM\n users\n INNER JOIN\n acounts\n ON\n accounts.user_id = users.id
# 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. As of Rails 4.2, ActiveRecord supports foreign key constraints natively.
-
When writing constructive migrations (adding tables or columns), use the
change
method instead ofup
anddown
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 prefered 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.
- Never make complex formatting in the views, export the formatting to a method in the view helper or the model.
-
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.
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 folderviews
.
-
When organization 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 localization 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 ofI18n.translate
andI18n.l
instead ofI18n.localize
.
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.
# bad
I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages]
# good
I18n.t 'activerecord.errors.messages.record_invalid'
- More detailed information about the Rails I18n can be found in the Rails Guides
Use the assets pipeline to leverage organization within your application.
- When possible, use gemified versions of assets (e.g. jquery-rails, jquery-ui-rails, bootstrap-sass, zurb-foundation).
-
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.
- 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
}
# 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) %>
# in your mailer class
default from: 'Your Name <info@your_site.com>'
# config/environments/test.rb
config.action_mailer.delivery_method = :test
# 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 with the help of sidekiq gem.
config.time_zone = 'Eastern European Time'
# optional - note it can be only :utc or :local (default is :utc)
config.active_record.default_timezone = :local
# bad
Time.parse('2015-03-02 19:05:37') # => Will assume time string given is in the system's time zone.
# good
Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00
# bad
Time.now # => Returns system time and ignores your configured time zone.
# good
Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00
Time.current # Same thing but shorter.
- 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.
-
OS-specific gems will by default result in a constantly changing
Gemfile.lock
for projects with multiple developers using different operating systems. Add all OS X specific gems to adarwin
group in the Gemfile, and all Linux specific gems to alinux
group:
# Gemfile
group :darwin do
gem 'rb-fsevent'
gem 'growl'
end
group :linux do
gem 'rb-inotify'
end
To require the appropriate gems in the right environment, add the
following to config/application.rb
:
platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym
Bundler.require(platform)
-
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 abundle install
.
This is a list of gems that are either problematic or superseded by other gems. You should avoid using them in your projects.
-
rmagick - this gem is notorious for its memory consumption. Use minimagick instead.
-
autotest - old solution for running tests automatically. Far inferior to guard and watchr.
-
rcov - code coverage tool, not compatible with Ruby 1.9. Use SimpleCov instead.
-
therubyracer - the use of this gem in production is strongly discouraged as it uses a very large amount of memory. I'd suggest using
node.js
instead.
This list is also a work in progress. Please, let me know if you know other popular, but flawed gems.
- If your projects depends on various external processes use foreman to manage them.
There are a few excellent resources on Rails style, that you should consider if you have time to spare:
- The Rails 4 Way
- Ruby on Rails Guides
- The RSpec Book
- The Cucumber Book
- Everyday Rails Testing with RSpec
- Better Specs for RSpec
Nothing written in this guide is set in stone. It's my desire to work together with everyone interested in Rails coding style, so that we could ultimately create a resource that will be beneficial to the entire Ruby community.
Feel free to open tickets or send pull requests with improvements. Thanks in advance for your help!
You can also support the project (and RuboCop) with financial contributions via gittip.
It's easy, just follow the contribution guidelines.
This work is licensed under a Creative Commons Attribution 3.0 Unported License
A community-driven style guide is of little use to a community that doesn't know about its existence. Tweet about the guide, share it with your friends and colleagues. Every comment, suggestion or opinion we get makes the guide just a little bit better. And we want to have the best possible guide, don't we?
Cheers,
Bozhidar