By Bozhidar Batsov
Role models are important.
— Officer Alex J. Murphy / RoboCop
Tip
|
You can find a beautiful version of this guide with much improved navigation at https://rails.rubystyle.guide. |
The goal of this guide is to present a set of best practices and style prescriptions for Ruby on Rails development. It’s a complementary guide to the already existing community-driven Ruby coding style guide.
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.
Note
|
Some of the advice here is applicable only to recent versions of Rails. |
You can generate a PDF copy of this guide using AsciiDoctor PDF, and an HTML copy with AsciiDoctor using the following commands:
# Generates README.pdf
asciidoctor-pdf -a allow-uri-read README.adoc
# Generates README.html
asciidoctor README.adoc
Tip
|
Install the gem install rouge |
Translations of the guide are available in the following languages:
Japanese
Russian
Tip
|
RuboCop, a static code analyzer (linter) and formatter, has a rubocop-rails extension, based on this style guide.
|
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.
When upgrading to a newer Rails version, your application’s configuration setting will remain on the previous version. To take advantage of the latest recommended Rails practices, the config.load_defaults
setting should match your Rails version.
# good
config.load_defaults 6.1
Avoid creating additional environment configurations than the defaults of development
, test
and production
.
If you need a production-like environment such as staging, use environment variables for configuration options.
Keep any additional configuration in YAML files under the config/
directory.
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
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 nested routes to express better the relationship between Active Record models.
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
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 URLs posts/1/comments/5/versions/7/edit
and you from long URL helpers edit_post_comment_version
.
resources :posts, shallow: true do
resources :comments do
resources :versions
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.
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.
Minimize the number of instance variables passed between a controller and a view.
Controller actions specified in the option of Action Filter should be in lexical scope. The ActionFilter specified for an inherited action makes it difficult to understand the scope of its impact on that action.
# bad
class UsersController < ApplicationController
before_action :require_login, only: :export
end
# good
class UsersController < ApplicationController
before_action :require_login, only: :export
def export
end
end
Prefer using a template over inline rendering.
# very bad
class ProductsController < ApplicationController
def index
render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>", type: :erb
end
end
# good
## app/views/products/index.html.erb
<%= render partial: 'product', collection: products %>
## app/views/products/_product.html.erb
<p><%= product.name %></p>
<p><%= product.price %></p>
## app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
render :index
end
end
Prefer render plain:
over render text:
.
# bad - sets MIME type to `text/html`
...
render text: 'Ruby!'
...
# bad - requires explicit MIME type declaration
...
render text: 'Ruby!', content_type: 'text/plain'
...
# good - short and precise
...
render plain: 'Ruby!'
...
Prefer corresponding symbols to numeric HTTP status codes. They are meaningful and do not look like "magic" numbers for less known HTTP status codes.
# bad
...
render status: 403
...
# good
...
render status: :forbidden
...
Introduce non-Active Record model classes freely.
Name the models with meaningful (but short) names without abbreviations.
If you need objects that support ActiveRecord-like behavior (like validations) without the database functionality, use ActiveModel::Model
.
class Message
include ActiveModel::Model
attr_accessor :name, :email, :content, :priority
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
Starting with Rails 6.1, you can also extend the attributes API from ActiveRecord using ActiveModel::Attributes
.
class Message
include ActiveModel::Model
include ActiveModel::Attributes
attribute :name, :string
attribute :email, :string
attribute :content, :string
attribute :priority, :integer
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
Unless they have some meaning in the business domain, don’t put methods in your model that just format your data (like code generating HTML). These methods are most likely going to be called from the view layer only, so their place is in helpers. Keep your models for business logic and data-persistence only.
Avoid altering Active Record 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 < ApplicationRecord
self.table_name = 'order'
...
end
ignored_columns
Avoid setting ignored_columns
. It may overwrite previous assignments and that is almost always a mistake. Prefer appending to the list instead.
class Transaction < ApplicationRecord
# bad - it may overwrite previous assignments
self.ignored_columns = %i[legacy]
# good - the value is appended to the list
self.ignored_columns += %i[legacy]
...
end
Prefer using the hash syntax for enum
. Array makes the database values implicit
& any insertion/removal/rearrangement of values in the middle will most probably
lead to broken code.
class Transaction < ApplicationRecord
# bad - implicit values - ordering matters
enum type: %i[credit debit]
# good - explicit values - ordering does not matter
enum type: {
credit: 0,
debit: 1
}
end
Group macro-style methods (has_many
, validates
, etc) in the beginning of the class definition.
class User < ApplicationRecord
# 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
# Rails 4+ enums after attr macros
enum role: { user: 0, moderator: 1, admin: 2 }
# 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: /AS{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
has_many :through
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 < ApplicationRecord
has_and_belongs_to_many :groups
end
class Group < ApplicationRecord
has_and_belongs_to_many :users
end
# preferred way - using has_many :through
class User < ApplicationRecord
has_many :memberships
has_many :groups, through: :memberships
end
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
end
class Group < ApplicationRecord
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
Always use the "new-style" validations.
# bad
validates_presence_of :email
validates_length_of :email, maximum: 100
# good
validates :email, presence: true, length: { maximum: 100 }
When naming custom validation methods, adhere to the simple rules:
validate :method_name
reads like a natural statement
the method name explains what it checks
the method is recognizable as a validation method by its name, not a predicate method
# good
validate :expiration_date_cannot_be_in_the_past
validate :discount_cannot_be_greater_than_total_value
validate :ensure_same_topic_is_chosen
# also good - explicit prefix
validate :validate_birthday_in_past
validate :validate_sufficient_quantity
validate :must_have_owner_with_no_other_items
validate :must_have_shipping_units
# bad
validate :birthday_in_past
validate :owner_has_no_other_items
To make validations easy to read, don’t list multiple attributes per validation.
# bad
validates :email, :password, presence: true
validates :email, length: { maximum: 100 }
# good
validates :email, presence: true, length: { maximum: 100 }
validates :password, 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
.
Consider extracting custom validators to a shared gem if you’re maintaining several related apps or the validators are generic enough.
Use named scopes freely.
class User < ApplicationRecord
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 < ApplicationRecord
def self.with_orders
joins(:orders).select('distinct(users.id)')
end
end
Order callback declarations in the order in which they will be executed. For reference, see Available Callbacks.
# bad
class Person
after_commit :after_commit_callback
before_validation :before_validation_callback
end
# good
class Person
before_validation :before_validation_callback
after_commit :after_commit_callback
end
Beware of the behavior of the following methods. They do not run the model validations and could easily corrupt the model state.
# bad
Article.first.decrement!(:view_count)
DiscussionBoard.decrement_counter(:post_count, 5)
Article.first.increment!(:view_count)
DiscussionBoard.increment_counter(:post_count, 5)
person.toggle :active
product.touch
Billing.update_all("category = 'authorized', author = 'David'")
user.update_attribute(:website, 'example.com')
user.update_columns(last_request_at: Time.current)
Post.update_counters 5, comment_count: -1, action_count: 1
# good
user.update_attributes(website: 'example.com')
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.
to_param
Method of the ModelThis 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.
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 Active Record.
friendly_id
GemIt allows creation of human-readable URLs by using some descriptive attribute of the model instead of its id
.
class Person
extend FriendlyId
friendly_id :name, use: :slugged
end
Check the gem documentation for more information about its usage.
find_each
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 |