A mostly reasonable approach to Ruby on Rails
(forked from bbatsov's 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):
```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.
-
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
-
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 ActiveRecord models.
class Post < ActiveRecord::Base has_many :comments end class Comments < ActiveRecord::Base belongs_to :post end # routes.rb resources :posts do resources :comments 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. It's removed from Rails 4.
- 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.
-
Introduce non-ActiveRecord model classes freely.
-
Name the models with meaningful (but short) names without abbreviations.
-
If you need model objects that support ActiveRecord behavior(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.
-
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
tohas_and_belongs_to_many
. Usinghas_many :through
allows additional attributes and validations on the join model.# 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
-
Prefer
self[:attribute]
overread_attribute(:attribute)
.# bad def amount read_attribute(:amount) * 100 end # good def amount self[:amount] * 100 end
-
Always use the new "sexy" validations.
# 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
. -
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 < ActiveRecord::Base scope :active, -> { where(active: true) } scope :inactive, -> { where(active: false) } scope :with_orders, -> { joins(:orders).select('distinct(users.id)') } end
-
Wrap named scopes in
lambdas
to initialize them lazily (this is only a prescription in Rails 3, but is mandatory in Rails 4).# bad class User < ActiveRecord::Base scope :active, where(active: true) scope :inactive, where(active: false) scope :with_orders, joins(:orders).select('distinct(users.id)') end # good 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 behavior of the
update_attribute
method. It doesn't run the model validations (unlikeupdate_attributes
) and could easily corrupt the model state. The method was finally deprecated in Rails 3.2.7 and does not exist in Rails 4. -
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.```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. Theid
of the object needs to be at the beginning so that it can be found by thefind
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
.```Ruby 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.all.find_each do |person| person.do_awesome_stuff end Person.where("age > 21").find_each do |person| person.party_all_night! end
-
Keep the
schema.rb
(orstructure.sql
) under version control. -
Use
rake db:schema:load
instead ofrake db:migrate
to initialize an empty database. -
Use
rake db:test:prepare
to update the schema of the test 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.
-
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.
- Avoid calling 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.
-
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" andUser.human_attribute_name("name")
will return "Full name". These translations of the attributes will be used as labels in the views. -
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
. -
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 templateapp/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] (http://guides.rubyonrails.org/i18n.html)
Use the assets pipeline to leverage organization within your application.
- Reserve
app/assets
for custom stylesheets, javascripts, or images. - Use
lib/assets
for your own libraries, that doesn’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).
-
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.
-
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
.
Here's a list of some "priceless" gems that we use at Avvo:
-
better_errors - Better Errors replaces the standard Rails error page with a much better and more useful error page. It is also usable outside of Rails in any Rack app as Rack middleware.
-
capybara - Capybara aims to simplify the process of integration testing Rack applications, such as Rails, Sinatra or Merb. Capybara simulates how a real user would interact with a web application. It is agnostic about the driver running your tests and currently comes with Rack::Test and Selenium support built in. HtmlUnit, WebKit and env.js are supported through external gems.
-
carrierwave - the ultimate file upload solution for Rails. Support both local and cloud storage for the uploaded files (and many other cool things). Integrates great with ImageMagick for image post-processing.
-
compass-rails - Great gem that adds support for some css frameworks. Includes collection of sass mixins that reduces code of css files and help fight with browser incompatibilities.
-
factory_girl - Nice and mature fixture replacement. Spiritual ancestor of fabrication.
-
kaminari - Great paginating solution.
-
simplecov - code coverage tool; generates great reports.
-
simplecov-rcov - RCov formatter for SimpleCov. Useful if you're trying to use SimpleCov with the Hudson contininous integration server.
-
slim - Slim is a concise templating language, considered by many far superior to HAML (not to mention Erb). Its performance is phenomenal.
This list is not exhaustive and other gems might be added to it along the road. All of the gems on the list are field tested, have active development and community and are known to be of good code quality.
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.
needs documentation
This work is licensed under a Creative Commons Attribution 3.0 Unported License