Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/arbox/rails-style-guide
:green_book: Russian Version: A community-driven Rails 3 & 4 & 5 style guide.
https://github.com/arbox/rails-style-guide
bbatsov rails ruby style-guide
Last synced: 4 days ago
JSON representation
:green_book: Russian Version: A community-driven Rails 3 & 4 & 5 style guide.
- Host: GitHub
- URL: https://github.com/arbox/rails-style-guide
- Owner: arbox
- Created: 2014-06-29T16:06:13.000Z (over 10 years ago)
- Default Branch: master
- Last Pushed: 2022-03-12T19:53:39.000Z (almost 3 years ago)
- Last Synced: 2024-12-10T19:41:41.041Z (11 days ago)
- Topics: bbatsov, rails, ruby, style-guide
- Language: Ruby
- Homepage: https://github.com/arbox/rails-style-guide
- Size: 480 KB
- Stars: 200
- Watchers: 22
- Forks: 40
- Open Issues: 3
-
Metadata Files:
- Readme: README-enUS.md
- Contributing: CONTRIBUTING-ruRU.md
Awesome Lists containing this project
README
# Prelude
> Role models are important.
> -- Officer Alex J. Murphy / RoboCopThe 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](https://github.com/rubocop-hq/ruby-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
[Pandoc](http://pandoc.org/).Translations of the guide are available in the following languages:
* [Chinese Simplified](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md)
* [Chinese Traditional](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md)
* [Japanese](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md)
* [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md)
* [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md)
* [Korean](https://github.com/pureugong/rails-style-guide/blob/master/README-koKR.md)
* [Vietnamese](https://github.com/CQBinh/rails-style-guide/blob/master/README-viVN.md)
* [Portuguese (pt-BR)](https://github.com/abraaomiranda/rails-style-guide/blob/master/README-ptBR.md)# The Rails 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.## Table of Contents
* [Configuration](#configuration)
* [Routing](#routing)
* [Controllers](#controllers)
* [Rendering](#rendering)
* [Models](#models)
* [ActiveRecord](#activerecord)
* [ActiveRecord Queries](#activerecord-queries)
* [Migrations](#migrations)
* [Views](#views)
* [Internationalization](#internationalization)
* [Assets](#assets)
* [Mailers](#mailers)
* [Active Support Core Extensions](#active-support-core-extensions)
* [Time](#time)
* [Bundler](#bundler)
* [Managing processes](#managing-processes)## Configuration
*
Put custom initialization code in `config/initializers`. The code in
initializers executes on application startup.
[[link](#config-initializers)]*
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.
[[link](#gem-initializers)]*
Adjust accordingly the settings for development, test and production
environment (in the corresponding files under `config/environments/`)
[[link](#dev-test-prod-configs)]* Mark additional assets for precompilation (if any):
```ruby
# 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.
[[link](#app-config)]*
Create an additional `staging` environment that closely resembles the
`production` one.
[[link](#staging-like-prod)]*
Keep any additional configuration in YAML files under the `config/` directory.
[[link](#yaml-config)]Since Rails 4.2 YAML configuration files can be easily loaded with the new `config_for` method:
```ruby
Rails::Application.config_for(:yaml_file)
```## Routing
*
When you need to add more actions to a RESTful resource (do you really need
them at all?) use `member` and `collection` routes.
[[link](#member-collection-routes)]```ruby
# 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.
[[link](#many-member-collection-routes)]```ruby
resources :subscriptions do
member do
get 'unsubscribe'
# more routes
end
endresources :photos do
collection do
get 'search'
# more routes
end
end
```*
Use nested routes to express better the relationship between ActiveRecord
models.
[[link](#nested-routes)]```ruby
class Post < ActiveRecord::Base
has_many :comments
endclass Comment < 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 urls `posts/1/comments/5/versions/7/edit` and you from long url helpers `edit_post_comment_version`.```ruby
resources :posts, shallow: true do
resources :comments do
resources :versions
end
end
```*
Use namespaced routes to group related actions.
[[link](#namespaced-routes)]```ruby
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.
[[link](#no-wild-routes)]```ruby
# 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.
[[link](#no-match-routes)]## 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).
[[link](#skinny-controllers)]*
Each controller action should (ideally) invoke only one method other than an
initial find or new.
[[link](#one-method)]*
Share no more than two instance variables between a controller and a view.
[[link](#shared-instance-variables)]*
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.
[[link](#lexically-scoped-action-filter)]```ruby
# bad
class UsersController < ApplicationController
before_action :require_login, only: :export
end# good
class UsersController < ApplicationController
before_action :require_login, only: :exportdef export
end
end
```### Rendering
*
Prefer using a template over inline rendering.
[[link](#inline-rendering)]```ruby
# very bad
class ProductsController < ApplicationController
def index
render inline: "<% products.each do |p| %><%= p.name %>
<% end %>", type: :erb
end
end# good
## app/views/products/index.html.erb
<%= render partial: 'product', collection: products %>## app/views/products/_product.html.erb
<%= product.name %>
<%= product.price %>
## app/controllers/foo_controller.rb
class ProductsController < ApplicationController
def index
render :index
end
end
```*
Prefer `render plain:` over `render text:`.
[[link](#plain-text-rendering)]```ruby
# 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](https://gist.github.com/mlanett/a31c340b132ddefa9cca) to numeric HTTP status codes. They are meaningful and do not look like "magic" numbers for less known HTTP status codes.
[[link](#http-status-code-symbols)]```ruby
# bad
...
render status: 403
...# good
...
render status: :forbidden
...
```## Models
*
Introduce non-ActiveRecord model classes freely.
[[link](#model-classes)]*
Name the models with meaningful (but short) names without abbreviations.
[[link](#meaningful-model-names)]*
If you need model objects that support ActiveRecord behavior (like validation)
without the ActiveRecord database functionality use the
[ActiveAttr](https://github.com/cgriego/active_attr) gem.
[[link](#activeattr-gem)]```ruby
class Message
include ActiveAttr::Modelattribute :name
attribute :email
attribute :content
attribute :priorityattr_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](http://railscasts.com/episodes/326-activeattr).*
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.
[[link](#model-business-logic)]### 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).
[[link](#keep-ar-defaults)]```ruby
# 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.
[[link](#macro-style-methods)]```ruby
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_birthattr_accessible :login, :first_name, :last_name, :email, :password
# Rails4+ enums after attr macros, prefer the hash syntax
enum gender: { female: 0, male: 1 }# followed by association macros
belongs_to :countryhas_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.
[[link](#has-many-through)]```ruby
# not so good - using has_and_belongs_to_many
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
endclass 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
endclass Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
endclass Group < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships
end
```*
Prefer `self[:attribute]` over `read_attribute(:attribute)`.
[[link](#read-attribute)]```ruby
# bad
def amount
read_attribute(:amount) * 100
end# good
def amount
self[:amount] * 100
end
```*
Prefer `self[:attribute] = value` over `write_attribute(:attribute, value)`.
[[link](#write-attribute)]```ruby
# bad
def amount
write_attribute(:amount, 100)
end# good
def amount
self[:amount] = 100
end
```*
Always use the new ["sexy"
validations](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/).
[[link](#sexy-validations)]```ruby
# bad
validates_presence_of :email
validates_length_of :email, maximum: 100# good
validates :email, presence: true, length: { maximum: 100 }
```*
To make validations easy to read, don't list multiple attributes per
validation
[[link](#single-attribute-validations)]```ruby
# 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.
[[link](#custom-validator-file)]```ruby
# 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
endclass Person
validates :email, email: true
end
```*
Keep custom validators under `app/validators`.
[[link](#app-validators)]*
Consider extracting custom validators to a shared gem if you're maintaining
several related apps or the validators are generic enough.
[[link](#custom-validators-gem)]*
Use named scopes freely.
[[link](#named-scopes)]```ruby
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.
[[link](#named-scope-class)]```ruby
class User < ActiveRecord::Base
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
referenece, see [Available Callbacks](http://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks)
[[link](#callbacks-order)]```Ruby
#bad
class Person
after_commit/after_rollback :after_commit_callback
after_save :after_save_callback
around_save :around_save_callback
after_update :after_update_callback
before_update :before_update_callback
after_validation :after_validation_callback
before_validation :before_validation_callback
before_save :before_save_callback
before_create :before_create_callback
after_create :after_create_callback
around_create :around_create_callback
around_update :around_update_callback
end#good
class Person
before_validation :before_validation_callback
after_validation :after_validation_callback
before_save :before_save_callback
around_save :around_save_callback
before_create :before_create_callback
around_create :around_create_callback
after_create :after_create_callback
before_update :before_update_callback
around_update :around_update_callback
after_update :after_update_callback
after_save :after_save_callback
after_commit/after_rollback :after_commit_callback
end
```*
Beware of the behavior of the
[following](http://guides.rubyonrails.org/active_record_validations.html#skipping-validations)
methods. They do not run the model validations and
could easily corrupt the model state.
[[link](#beware-skip-model-validations)]```ruby
# 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:
[[link](#user-friendly-urls)]* 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.```ruby
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 its `id`.```ruby
class Person
extend FriendlyId
friendly_id :name, use: :slugged
end
```Check the [gem documentation](https://github.com/norman/friendly_id) 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 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.
[[link](#find-each)]```ruby
# bad
Person.all.each do |person|
person.do_awesome_stuff
endPerson.where('age > 21').each do |person|
person.party_all_night!
end# good
Person.find_each do |person|
person.do_awesome_stuff
endPerson.where('age > 21').find_each do |person|
person.party_all_night!
end
```*
Since [Rails creates callbacks for dependent
associations](https://github.com/rails/rails/issues/3458), always call
`before_destroy` callbacks that perform validation with `prepend: true`.
[[link](#before_destroy)]```ruby
# bad (roles will be deleted automatically even if super_admin? is true)
has_many :roles, dependent: :destroybefore_destroy :ensure_deletable
def ensure_deletable
raise "Cannot delete super admin." if super_admin?
end# good
has_many :roles, dependent: :destroybefore_destroy :ensure_deletable, prepend: true
def ensure_deletable
raise "Cannot delete super admin." if super_admin?
end
```*
Define the `dependent` option to the `has_many` and `has_one` associations.
[[link](#has_many-has_one-dependent-option)]```ruby
# bad
class Post < ActiveRecord::Base
has_many :comments
end# good
class Post < ActiveRecord::Base
has_many :comments, dependent: :destroy
end
```*
When persisting AR objects always use the exception raising bang! method or handle the method return value.
This applies to `create`, `save`, `update`, `destroy`, `first_or_create` and `find_or_create_by`.
[[link](#save-bang)]```ruby
# bad
user.create(name: 'Bruce')# bad
user.save# good
user.create!(name: 'Bruce')
# or
bruce = user.create(name: 'Bruce')
if bruce.persisted?
...
else
...
end# good
user.save!
# or
if user.save
...
else
...
end
```### ActiveRecord Queries
*
Avoid string interpolation in
queries, as it will make your code susceptible to SQL injection
attacks.
[[link](#avoid-interpolation)]```ruby
# 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.
[[link](#named-placeholder)]```ruby
# 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]
)
```*
Favor the use of `find` over `where.take!`, `find_by!`, and `find_by_id!`
when you need to retrieve a single record by primary key id and raise
`ActiveRecord::RecordNotFound` when the record is not found.
[[link](#find)]```ruby
# bad
User.where(id: id).take!# bad
User.find_by_id!(id)# bad
User.find_by!(id: id)# good
User.find(id)
```*
Favor the use of `find_by` over `where.take` and `find_by_attribute`
when you need to retrieve a single record by one or more attributes and return
`nil` when the record is not found.
[[link](#find_by)]```ruby
# bad
User.where(id: id).take
User.where(first_name: 'Bruce', last_name: 'Wayne').take# bad
User.find_by_id(id)
# bad, deprecated in ActiveRecord 4.0, removed in 4.1+
User.find_by_first_name_and_last_name('Bruce', 'Wayne')# good
User.find_by(id: id)
User.find_by(first_name: 'Bruce', last_name: 'Wayne')
```*
Favor the use of `where.not` over SQL.
[[link](#where-not)]```ruby
# bad
User.where("id != ?", id)# good
User.where.not(id: id)
```*
Don't use the `id` column for ordering. The sequence of ids is not
guaranteed to be in any particular order, despite often (incidentally)
being chronological. Use a timestamp column to order chronologically.
As a bonus the intent is clearer.
[[link](#order-by-id)]```ruby
# bad
scope :chronological, -> { order(id: :asc) }# good
scope :chronological, -> { order(created_at: :asc) }
```*
Favor the use of `ids` over `pluck(:id)`.
[[link](#ids)]```Ruby
# bad
User.pluck(:id)# good
User.ids
```*
When specifying an explicit query in a method such as `find_by_sql`, use
heredocs with `squish`. 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).
[[link](#squished-heredocs)]```ruby
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`](http://apidock.com/rails/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
```*
When querying ActiveRecord collections, prefer `size` (selects between count/length behavior based on whether collection is already loaded) or `length` (always loads the whole collection and counts the array elements) over `count` (always does a database query for the count).
[[link](#size-over-count-or-length)]```ruby
# bad
User.count# good
User.all.size# good - if you really need to load all users into memory
User.all.length
```## Migrations
*
Keep the `schema.rb` (or `structure.sql`) under version control.
[[link](#schema-version)]*
Use `rake db:schema:load` instead of `rake db:migrate` to initialize an empty
database.
[[link](#db-schema-load)]*
Enforce default values in the migrations themselves instead of in the
application layer.
[[link](#default-migration-values)]```ruby
# bad - application enforced default value
class Product < ActiveRecord::Base
def amount
self[:amount] || 0
end
end# good - database enforced
class AddDefaultAmountToProducts < ActiveRecord::Migration
def change
change_column_default :products, :amount, 0
end
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.
[[link](#foreign-key-constraints)]*
When writing constructive migrations (adding tables or columns),
use the `change` method instead of `up` and `down` methods.
[[link](#change-vs-up-down)]```ruby
# the old way
class AddNameToPeople < ActiveRecord::Migration
def up
add_column :people, :name, :string
enddef down
remove_column :people, :name
end
end# the new preferred way
class AddNameToPeople < ActiveRecord::Migration
def change
add_column :people, :name, :string
end
end
```*
If you have to use models in migrations, make sure you define them
so that you don't end up with broken migrations in the future
[[link](#define-model-class-migrations)]```ruby
# db/migrate/.rb
# frozen_string_literal: true# bad
class ModifyDefaultStatusForProducts < ActiveRecord::Migration
def change
old_status = 'pending_manual_approval'
new_status = 'pending_approval'reversible do |dir|
dir.up do
Product.where(status: old_status).update_all(status: new_status)
change_column :products, :status, :string, default: new_status
enddir.down do
Product.where(status: new_status).update_all(status: old_status)
change_column :products, :status, :string, default: old_status
end
end
end
end# good
# Define `table_name` in a custom named class to make sure that
# you run on the same table you had during the creation of the migration.
# In future if you override the `Product` class
# and change the `table_name`, it won't break
# the migration or cause serious data corruption.
class MigrationProduct < ActiveRecord::Base
self.table_name = :products
endclass ModifyDefaultStatusForProducts < ActiveRecord::Migration
def change
old_status = 'pending_manual_approval'
new_status = 'pending_approval'reversible do |dir|
dir.up do
MigrationProduct.where(status: old_status).update_all(status: new_status)
change_column :products, :status, :string, default: new_status
enddir.down do
MigrationProduct.where(status: new_status).update_all(status: old_status)
change_column :products, :status, :string, default: old_status
end
end
end
end
```*
Name your foreign keys explicitly instead of relying on Rails auto-generated
FK names. (http://guides.rubyonrails.org/active_record_migrations.html#foreign-keys)
[[link](#meaningful-foreign-key-naming)]```ruby
# bad
class AddFkArticlesToAuthors < ActiveRecord::Migration
def change
add_foreign_key :articles, :authors
end
end# good
class AddFkArticlesToAuthors < ActiveRecord::Migration
def change
add_foreign_key :articles, :authors, name: :articles_author_id_fk
end
end
```*
Don't use non-reversible migration commands in the `change` method.
Reversible migration commands are listed below.
[ActiveRecord::Migration::CommandRecorder](http://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html)
[[link](#reversible-migration)]```ruby
# bad
class DropUsers < ActiveRecord::Migration
def change
drop_table :users
end
end# good
class DropUsers < ActiveRecord::Migration
def up
drop_table :users
enddef down
create_table :users do |t|
t.string :name
end
end
end# good
# In this case, block will be used by create_table in rollback
# http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table
class DropUsers < ActiveRecord::Migration
def change
drop_table :users do |t|
t.string :name
end
end
end
```## Views
*
Never call the model layer directly from a view.
[[link](#no-direct-model-view)]*
Never make complex formatting in the views, export the formatting to a method
in the view helper or the model.
[[link](#no-complex-view-formatting)]*
Mitigate code duplication by using partial templates and layouts.
[[link](#partials)]## Internationalization
*
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.
[[link](#locale-texts)]*
When the labels of an ActiveRecord model need to be translated, use the
`activerecord` scope:
[[link](#translated-labels)]```
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 `locales/models` and the
texts used in the views in folder `locales/views`.
[[link](#organize-locale-files)]* 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.```ruby
# 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.
[[link](#shared-localization)]*
Use the short form of the I18n methods: `I18n.t` instead of `I18n.translate`
and `I18n.l` instead of `I18n.localize`.
[[link](#short-i18n)]*
Use "lazy" lookup for the texts used in views. Let's say we have the following
structure:
[[link](#lazy-lookup)]```
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:```ruby
= 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.
[[link](#dot-separated-keys)]```ruby
# 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](http://guides.rubyonrails.org/i18n.html)
[[link](#i18n-guides)]## Assets
Use the [assets pipeline](http://guides.rubyonrails.org/asset_pipeline.html) to leverage organization within
your application.*
Reserve `app/assets` for custom stylesheets, javascripts, or images.
[[link](#reserve-app-assets)]*
Use `lib/assets` for your own libraries that don’t really fit into the
scope of the application.
[[link](#lib-assets)]*
Third party code such as [jQuery](http://jquery.com/) or
[bootstrap](http://twitter.github.com/bootstrap/) should be placed in
`vendor/assets`.
[[link](#vendor-assets)]*
When possible, use gemified versions of assets (e.g.
[jquery-rails](https://github.com/rails/jquery-rails),
[jquery-ui-rails](https://github.com/joliss/jquery-ui-rails),
[bootstrap-sass](https://github.com/thomas-mcdonald/bootstrap-sass),
[zurb-foundation](https://github.com/zurb/foundation)).
[[link](#gem-assets)]## 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.
[[link](#mailer-name)]*
Provide both HTML and plain-text view templates.
[[link](#html-plain-email)]*
Enable errors raised on failed mail delivery in your development environment.
The errors are disabled by default.
[[link](#enable-delivery-errors)]```ruby
# config/environments/development.rbconfig.action_mailer.raise_delivery_errors = true
```*
Use a local SMTP server like
[Mailcatcher](https://github.com/sj26/mailcatcher) in the development
environment.
[[link](#local-smtp)]```ruby
# config/environments/development.rbconfig.action_mailer.smtp_settings = {
address: 'localhost',
port: 1025,
# more settings
}
```*
Provide default settings for the host name.
[[link](#default-hostname)]```ruby
# 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.
[[link](#url-not-path-in-email)]```ruby
# 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:
[[link](#email-addresses)]```ruby
# in your mailer class
default from: 'Your Name '
```*
Make sure that the e-mail delivery method for your test environment is set to
`test`:
[[link](#delivery-method-test)]```ruby
# config/environments/test.rbconfig.action_mailer.delivery_method = :test
```*
The delivery method for development and production should be `smtp`:
[[link](#delivery-method-smtp)]```ruby
# config/environments/development.rb, config/environments/production.rbconfig.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](https://github.com/fphilipe/premailer-rails) and
[roadie](https://github.com/Mange/roadie).
[[link](#inline-email-styles)]*
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](https://github.com/mperham/sidekiq) gem.
[[link](#background-email)]## Active Support Core Extensions
*
Prefer Ruby 2.3's safe navigation operator `&.` over `ActiveSupport#try!`.
[[link](#try-bang)]```ruby
# bad
obj.try! :fly# good
obj&.fly
```*
Prefer Ruby's Standard Library methods over `ActiveSupport` aliases.
[[link](#active_support_aliases)]```ruby
# bad
'the day'.starts_with? 'th'
'the day'.ends_with? 'ay'# good
'the day'.start_with? 'th'
'the day'.end_with? 'ay'
```*
Prefer Ruby's Standard Library over uncommon ActiveSupport extensions.
[[link](#active_support_extensions)]```ruby
# bad
(1..50).to_a.forty_two
1.in? [1, 2]
'day'.in? 'the day'# good
(1..50).to_a[41]
[1, 2].include? 1
'the day'.include? 'day'
```*
Prefer Ruby's comparison operators over ActiveSupport's `Array#inquiry`, and `String#inquiry`.
[[link](#inquiry)]```ruby
# bad - String#inquiry
ruby = 'two'.inquiry
ruby.two?# good
ruby = 'two'
ruby == 'two'# bad - Array#inquiry
pets = %w(cat dog).inquiry
pets.gopher?# good
pets = %w(cat dog)
pets.include? 'cat'
```## Time
*
Config your timezone accordingly in `application.rb`.
[[link](#tz-config)]```ruby
config.time_zone = 'Eastern European Time'
# optional - note it can be only :utc or :local (default is :utc)
config.active_record.default_timezone = :local
```*
Don't use `Time.parse`.
[[link](#time-parse)]```ruby
# 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
```*
Don't use [`String#to_time`](https://apidock.com/rails/String/to_time)
[[link](#to-time)]```ruby
# bad - assumes time string given is in the system's time zone.
'2015-03-02 19:05:37'.to_time# good
Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00
```*
Don't use `Time.now`.
[[link](#time-now)]```ruby
# 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.
```## Bundler
*
Put gems used only for development or testing in the appropriate group in the
Gemfile.
[[link](#dev-test-gems)]*
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.
[[link](#only-good-gems)]*
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 a `darwin` group in the Gemfile, and
all Linux specific gems to a `linux` group:
[[link](#os-specific-gemfile-locks)]```ruby
# Gemfile
group :darwin do
gem 'rb-fsevent'
gem 'growl'
endgroup :linux do
gem 'rb-inotify'
end
```To require the appropriate gems in the right environment, add the
following to `config/application.rb`:```ruby
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 a `bundle install`.
[[link](#gemfile-lock)]## Managing processes
*
If your projects depends on various external processes use
[foreman](https://github.com/ddollar/foreman) to manage them.
[[link](#foreman)]# Further Reading
There are a few excellent resources on Rails style, that you should consider if
you have time to spare:* [The Rails 4 Way](http://www.amazon.com/The-Rails-Addison-Wesley-Professional-Ruby/dp/0321944275)
* [Ruby on Rails Guides](http://guides.rubyonrails.org/)
* [The RSpec Book](http://pragprog.com/book/achbd/the-rspec-book)
* [The Cucumber Book](http://pragprog.com/book/hwcuc/the-cucumber-book)
* [Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec)
* [Rails 4 Test Prescriptions](https://pragprog.com/book/nrtest2/rails-4-test-prescriptions)
* [Better Specs for RSpec](http://betterspecs.org)# Contributing
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
[Patreon](https://www.patreon.com/bbatsov).## How to Contribute?
It's easy, just follow the [contribution guidelines](https://github.com/rubocop-hq/rails-style-guide/blob/master/CONTRIBUTING.md).
# License
![Creative Commons License](http://i.creativecommons.org/l/by/3.0/88x31.png)
This work is licensed under a [Creative Commons Attribution 3.0 Unported
License](http://creativecommons.org/licenses/by/3.0/deed.en_US)# Spread the Word
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](https://twitter.com/bbatsov)