- Notifications
You must be signed in to change notification settings - Fork1.1k
A community-driven Ruby on Rails style guide
rubocop/rails-style-guide
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Role models are important.
— Officer Alex J. Murphy / RoboCop
Tip | You can find a beautiful version of this guide with much improved navigation athttps://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-drivenRuby 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 usingAsciiDoctor PDF, and an HTML copywithAsciiDoctor using the following commands:
# Generates README.pdfasciidoctor-pdf -a allow-uri-read README.adoc# Generates README.htmlasciidoctor README.adoc
Tip | Install the gem install rouge |
Translations of the guide are available in the following languages:
Tip | RuboCop, a static code analyzer (linter) and formatter, has arubocop-rails extension, based on this style guide. |
Put custom initialization code inconfig/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 examplecarrierwave.rb
,active_admin.rb
, etc.
Adjust accordingly the settings for development, test and production environment (in the corresponding files underconfig/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.cssrails_admin/rails_admin.js)
Keep configuration that’s applicable to all environments in theconfig/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, theconfig.load_defaults
setting should match your Rails version.
# goodconfig.load_defaults6.1
Avoid creating additional environment configurations than the defaults ofdevelopment
,test
andproduction
.If you need a production-like environment such as staging, use environment variables for configuration options.
Keep any additional configuration in YAML files under theconfig/
directory.
Since Rails 4.2 YAML configuration files can be easily loaded with the newconfig_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?) usemember
andcollection
routes.
# badget'subscriptions/:id/unsubscribe'resources:subscriptions# goodresources:subscriptionsdoget'unsubscribe',on::memberend# badget'photos/search'resources:photos# goodresources:photosdoget'search',on::collectionend
If you need to define multiplemember/collection
routes use the alternative block syntax.
resources:subscriptionsdomemberdoget'unsubscribe'# more routesendendresources:photosdocollectiondoget'search'# more routesendend
Use nested routes to express better the relationship between Active Record models.
classPost <ApplicationRecordhas_many:commentsendclassComment <ApplicationRecordbelongs_to:postend# routes.rbresources:postsdoresources:commentsend
If you need to nest routes more than 1 level deep then use theshallow: 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:truedoresources:commentsdoresources:versionsendend
Use namespaced routes to group related actions.
namespace:admindo# Directs /admin/products/* to Admin::ProductsController# (app/controllers/admin/products_controller.rb)resources:productsend
Never use the legacy wild controller route.This route will make all actions in every controller accessible via GET requests.
# very badmatch':controller(/:action(/:id(.:format)))'
Don’t usematch
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.
# badclassUsersController <ApplicationControllerbefore_action:require_login,only::exportend# goodclassUsersController <ApplicationControllerbefore_action:require_login,only::exportdefexportendend
Prefer using a template over inline rendering.
# very badclassProductsController <ApplicationControllerdefindexrenderinline:"<% products.each do |p| %><p><%= p.name %></p><% end %>",type::erbendend# good## app/views/products/index.html.erb<%=renderpartial:'product',collection:products %>## app/views/products/_product.html.erb<p><%=product.name %></p><p><%= product.price %></p>## app/controllers/products_controller.rbclassProductsController <ApplicationControllerdefindexrender:indexendend
Preferrender plain:
overrender text:
.
# bad - sets MIME type to `text/html`...rendertext:'Ruby!'...# bad - requires explicit MIME type declaration...rendertext:'Ruby!',content_type:'text/plain'...# good - short and precise...renderplain:'Ruby!'...
Prefercorresponding symbols to numeric HTTP status codes.They are meaningful and do not look like "magic" numbers for less known HTTP status codes.
# bad...renderstatus:403...# good...renderstatus::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, useActiveModel::Model
.
classMessageincludeActiveModel::Modelattr_accessor:name,:email,:content,:priorityvalidates:name,presence:truevalidates: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 usingActiveModel::Attributes
.
classMessageincludeActiveModel::ModelincludeActiveModel::Attributesattribute:name,:stringattribute:email,:stringattribute:content,:stringattribute:priority,:integervalidates:name,presence:truevalidates: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 schemaclassTransaction <ApplicationRecordself.table_name='order' ...end
Avoid settingignored_columns
. It may overwrite previous assignments and that is almost always a mistake. Prefer appending to the list instead.
classTransaction <ApplicationRecord# bad - it may overwrite previous assignmentsself.ignored_columns=%i[legacy]# good - the value is appended to the listself.ignored_columns +=%i[legacy] ...end
Prefer using the hash syntax forenum
. Array makes the database values implicit& any insertion/removal/rearrangement of values in the middle will most probablylead to broken code.
classTransaction <ApplicationRecord# bad - implicit values - ordering mattersenumtype:%i[creditdebit]# good - explicit values - ordering does not matterenumtype:{credit:0,debit:1}end
Group macro-style methods (has_many
,validates
, etc) in the beginning of the class definition.
classUser <ApplicationRecord# keep the default scope first (if any)default_scope{where(active:true)}# constants come up nextCOLORS=%w(redgreenblue)# afterwards we put attr related macrosattr_accessor:formatted_date_of_birthattr_accessible:login,:first_name,:last_name,:email,:password# Rails 4+ enums after attr macrosenumrole:{user:0,moderator:1,admin:2}# followed by association macrosbelongs_to:countryhas_many:authentications,dependent::destroy# and validation macrosvalidates:email,presence:truevalidates:username,presence:truevalidates: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 callbacksbefore_save:cookbefore_save:update_username_lower# other macros (like devise's) should be placed after the callbacks ...end
Preferhas_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_manyclassUser <ApplicationRecordhas_and_belongs_to_many:groupsendclassGroup <ApplicationRecordhas_and_belongs_to_many:usersend# preferred way - using has_many :throughclassUser <ApplicationRecordhas_many:membershipshas_many:groups,through::membershipsendclassMembership <ApplicationRecordbelongs_to:userbelongs_to:groupendclassGroup <ApplicationRecordhas_many:membershipshas_many:users,through::membershipsend
Preferself[:attribute]
overread_attribute(:attribute)
.
# baddefamountread_attribute(:amount) *100end# gooddefamountself[:amount] *100end
Preferself[:attribute] = value
overwrite_attribute(:attribute, value)
.
# baddefamountwrite_attribute(:amount,100)end# gooddefamountself[:amount]=100end
Always use the"new-style" validations.
# badvalidates_presence_of:emailvalidates_length_of:email,maximum:100# goodvalidates:email,presence:true,length:{maximum:100}
When naming custom validation methods, adhere to the simple rules:
validate :method_name
reads like a natural statementthe method name explains what it checks
the method is recognizable as a validation method by its name, not a predicate method
# goodvalidate:expiration_date_cannot_be_in_the_pastvalidate:discount_cannot_be_greater_than_total_valuevalidate:ensure_same_topic_is_chosen# also good - explicit prefixvalidate:validate_birthday_in_pastvalidate:validate_sufficient_quantityvalidate:must_have_owner_with_no_other_itemsvalidate:must_have_shipping_units# badvalidate:birthday_in_pastvalidate:owner_has_no_other_items
To make validations easy to read, don’t list multiple attributes per validation.
# badvalidates:email,:password,presence:truevalidates:email,length:{maximum:100}# goodvalidates: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.
# badclassPersonvalidates:email,format:{with:/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i}end# goodclassEmailValidator <ActiveModel::EachValidatordefvalidate_each(record,attribute,value)record.errors[attribute] <<(options[:message] ||'is not a valid email')unlessvalue =~/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/iendendclassPersonvalidates:email,email:trueend
Keep custom validators underapp/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.
classUser <ApplicationRecordscope: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 anActiveRecord::Relation
object.Arguably you can define even simpler scopes like this.
classUser <ApplicationRecorddefself.with_ordersjoins(:orders).select('distinct(users.id)')endend
Order callback declarations in the order in which they will be executed.For reference, seeAvailable Callbacks.
# badclassPersonafter_commit:after_commit_callbackbefore_validation:before_validation_callbackend# goodclassPersonbefore_validation:before_validation_callbackafter_commit:after_commit_callbackend
Beware of the behavior of thefollowing methods.They do not run the model validations and could easily corrupt the model state.
# badArticle.first.decrement!(:view_count)DiscussionBoard.decrement_counter(:post_count,5)Article.first.increment!(:view_count)DiscussionBoard.increment_counter(:post_count,5)person.toggle:activeproduct.touchBilling.update_all("category = 'authorized', author = 'David'")user.update_attribute(:website,'example.com')user.update_columns(last_request_at:Time.current)Post.update_counters5,comment_count: -1,action_count:1# gooduser.update_attributes(website:'example.com')
Use user-friendly URLs.Show some descriptive attribute of the model in the URL rather than itsid
.There is more than one way to achieve this.
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.
classPersondefto_param"#{id}#{name}".parameterizeendend
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 Active Record.
It allows creation of human-readable URLs by using some descriptive attribute of the model instead of itsid
.
classPersonextendFriendlyIdfriendly_id:name,use::sluggedend
Check thegem documentation for more information about its usage.
Usefind_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.
# badPerson.all.eachdo |person|person.do_awesome_stuffendPerson.where('age > 21').eachdo |person|person.party_all_night!end# goodPerson.find_eachdo |person|person.do_awesome_stuffendPerson.where('age > 21').find_eachdo |person|person.party_all_night!end
SinceRails creates callbacks for dependent associations, always callbefore_destroy
callbacks that perform validation withprepend: true
.
# bad (roles will be deleted automatically even if super_admin? is true)has_many:roles,dependent::destroybefore_destroy:ensure_deletabledefensure_deletableraise"Cannot delete super admin."ifsuper_admin?end# goodhas_many:roles,dependent::destroybefore_destroy:ensure_deletable,prepend:truedefensure_deletableraise"Cannot delete super admin."ifsuper_admin?end
Define thedependent
option to thehas_many
andhas_one
associations.
# badclassPost <ApplicationRecordhas_many:commentsend# goodclassPost <ApplicationRecordhas_many:comments,dependent::destroyend
When persisting AR objects always use the exception raising bang! method or handle the method return value.This applies tocreate
,save
,update
,destroy
,first_or_create
andfind_or_create_by
.
# baduser.create(name:'Bruce')# baduser.save# gooduser.create!(name:'Bruce')# orbruce=user.create(name:'Bruce')ifbruce.persisted? ...else ...end# gooduser.save!# orifuser.save ...else ...end
Avoid string interpolation in queries, as it will make your code susceptible to SQL injection attacks.
# bad - param will be interpolated unescapedClient.where("orders_count =#{params[:orders]}")# good - param will be properly escapedClient.where('orders_count = ?',params[:orders])
Consider using named placeholders instead of positional placeholders when you have more than 1 placeholder in your query.
# okishClient.where('orders_count >= ? AND country_code = ?',params[:min_orders_count],params[:country_code])# goodClient.where('orders_count >= :min_orders_count AND country_code = :country_code',min_orders_count:params[:min_orders_count],country_code:params[:country_code])
Preferfind
overwhere.take!
,find_by!
, andfind_by_id!
when you need to retrieve a single record by primary key id and raiseActiveRecord::RecordNotFound
when the record is not found.
# badUser.where(id:id).take!# badUser.find_by_id!(id)# badUser.find_by!(id:id)# goodUser.find(id)
Preferfind_by
overwhere.take
andfind_by_attribute
when you need to retrieve a single record by one or more attributes and returnnil
when the record is not found.
# badUser.where(email:email).takeUser.where(first_name:'Bruce',last_name:'Wayne').take# badUser.find_by_email(email)User.find_by_first_name_and_last_name('Bruce','Wayne')# goodUser.find_by(email:email)User.find_by(first_name:'Bruce',last_name:'Wayne')
Prefer passing conditions towhere
andwhere.not
as a hash over using fragments of SQL.
# badUser.where("name = ?",name)# goodUser.where(name:name)# badUser.where("id != ?",id)# goodUser.where.not(id:id)
If you’re using Rails 6.1 or higher, usewhere.missing to find missing relationship records.
# badPost.left_joins(:author).where(authors:{id:nil})# goodPost.where.missing(:author)
Don’t use theid
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.
# badscope:chronological,->{order(id::asc)}# goodscope:chronological,->{order(created_at::asc)}
Usepluck to select a single value from multiple records.
# badUser.all.map(&:name)# badUser.all.map{ |user|user[:name]}# goodUser.pluck(:name)
Usepick to select a single value from a single record.
# badUser.pluck(:name).first# badUser.first.name# goodUser.pick(:name)
When specifying an explicit query in a method such asfind_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 accounts\n ON\n accounts.user_id = users.id
When querying Active Record collections, prefersize
(selects between count/length behavior based on whether collection is already loaded) orlength
(always loads the whole collection and counts the array elements) overcount
(always does a database query for the count).
# badUser.count# goodUser.all.size# good - if you really need to load all users into memoryUser.all.length
Use ranges instead of defining comparative conditions using a template for scalar values.
# badUser.where("created_at >= ?",30.days.ago).where("created_at <= ?",7.days.ago)User.where("created_at >= ? AND created_at <= ?",30.days.ago,7.days.ago)User.where("created_at >= :start AND created_at <= :end",start:30.days.ago,end:7.days.ago)# goodUser.where(created_at:30.days.ago..7.days.ago)# badUser.where("created_at >= ?",7.days.ago)# goodUser.where(created_at:7.days.ago..)# note - ranges are inclusive or exclusive of their ending, not beginningUser.where(created_at:7.days.ago..)# produces >=User.where(created_at:7.days.ago...)# also produces >=User.where(created_at: ..7.days.ago)# inclusive: produces <=User.where(created_at: ...7.days.ago)# exclusive: produces <# okish - there is no range syntax that would denote exclusion at the beginning of the rangeCustomer.where("purchases_count > :min AND purchases_count <= :max",min:0,max:5)
Note | Rails 6.0 or later is required for endless range Ruby 2.6 syntax, and Rails 6.0.3 for beginless range Ruby 2.7 syntax. |
Avoid passing multiple attributes towhere.not
. Rails logic in this case has changed in Rails 6.1 andwill now yield results matching either of those conditions,e.g.where.not(status: 'active', plan: 'basic')
would return records with active status when the plan is business.
# badUser.where.not(status:'active',plan:'basic')# goodUser.where.not('status = ? AND plan = ?','active','basic')
Usingall
as a receiver is redundant. The result won’t change withoutall
, so it should be removed.
# badUser.all.find(id)User.all.order(:created_at)users.all.where(id:ids)user.articles.all.order(:created_at)# goodUser.find(id)User.order(:created_at)users.where(id:ids)user.articles.order(:created_at)
Note | When the receiver forall is an association, there are methods whose behavior changes by omittingall . |
The following methods behave differently withoutall
:
delete
-with all /without alldelete_all
-with all /without alldestroy
-with all /without alldestroy_all
-with all /without all
So, when considering removingall
from the receiver of these methods, it is recommended to refer to the documentation to understand how the behavior changes.
Keep theschema.rb
(orstructure.sql
) under version control.
Userake db:schema:load
instead ofrake db:migrate
to initialize an empty database.
Enforce default values in the migrations themselves instead of in the application layer.
# bad - application enforced default valueclassProduct <ApplicationRecorddefamountself[:amount] ||0endend# good - database enforcedclassAddDefaultAmountToProducts <ActiveRecord::Migrationdefchangechange_column_default:products,:amount,0endend
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.
With SQL databases, if a boolean column is not given a default value, it will have three possible values:true
,false
andNULL
.Boolean operatorswork in unexpected ways withNULL
.
For example in SQL queries,true AND NULL
isNULL
(not false),true AND NULL OR false
isNULL
(not false). This can make SQL queries return unexpected results.
To avoid such situations, boolean columns should always have a default value and aNOT NULL
constraint.
# bad - boolean without a default valueadd_column:users,:active,:boolean# good - boolean with a default value (`false` or `true`) and with restricted `NULL`add_column:users,:active,:boolean,default:true,null:falseadd_column:users,:admin,:boolean,default:false,null:false
Enforce foreign-key constraints. As of Rails 4.2, Active Record supports foreign key constraints natively.
# bad - does not add foreign keyscreate_table:commentdo |t|t.references:articlet.belongs_to:usert.integer:category_idend# goodcreate_table:commentdo |t|t.references:article,foreign_key:truet.belongs_to:user,foreign_key:truet.references:category,foreign_key:{to_table::comment_categories}end
When writing constructive migrations (adding tables or columns), use thechange
method instead ofup
anddown
methods.
# the old wayclassAddNameToPeople <ActiveRecord::Migrationdefupadd_column:people,:name,:stringenddefdownremove_column:people,:nameendend# the new preferred wayclassAddNameToPeople <ActiveRecord::Migrationdefchangeadd_column:people,:name,:stringendend
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.
# db/migrate/<migration_file_name>.rb# frozen_string_literal: true# badclassModifyDefaultStatusForProducts <ActiveRecord::Migrationdefchangeold_status='pending_manual_approval'new_status='pending_approval'reversibledo |dir|dir.updoProduct.where(status:old_status).update_all(status:new_status)change_column:products,:status,:string,default:new_statusenddir.downdoProduct.where(status:new_status).update_all(status:old_status)change_column:products,:status,:string,default:old_statusendendendend# 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.classMigrationProduct <ActiveRecord::Baseself.table_name=:productsendclassModifyDefaultStatusForProducts <ActiveRecord::Migrationdefchangeold_status='pending_manual_approval'new_status='pending_approval'reversibledo |dir|dir.updoMigrationProduct.where(status:old_status).update_all(status:new_status)change_column:products,:status,:string,default:new_statusenddir.downdoMigrationProduct.where(status:new_status).update_all(status:old_status)change_column:products,:status,:string,default:old_statusendendendend
Name your foreign keys explicitly instead of relying on Rails auto-generated FK names. (https://guides.rubyonrails.org/active_record_migrations.html#foreign-keys)
# badclassAddFkArticlesToAuthors <ActiveRecord::Migrationdefchangeadd_foreign_key:articles,:authorsendend# goodclassAddFkArticlesToAuthors <ActiveRecord::Migrationdefchangeadd_foreign_key:articles,:authors,name::articles_author_id_fkendend
Don’t use non-reversible migration commands in thechange
method.Reversible migration commands are listed below.ActiveRecord::Migration::CommandRecorder
# badclassDropUsers <ActiveRecord::Migrationdefchangedrop_table:usersendend# goodclassDropUsers <ActiveRecord::Migrationdefupdrop_table:usersenddefdowncreate_table:usersdo |t|t.string:nameendendend# good# In this case, block will be used by create_table in rollback# https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_tableclassDropUsers <ActiveRecord::Migrationdefchangedrop_table:usersdo |t|t.string:nameendendend
Never call the model layer directly from a view.
Avoid complex formatting in the views.A view helper is useful for simple cases, but if it’s more complex then consider using a decorator or presenter.
Mitigate code duplication by using partial templates and layouts.
Avoid using instance variables in partials, pass a local variable torender
instead.The partial may be used in a different controller or action, where the variable can have a different name or even be absent.In these cases, an undefined instance variable will not raise an exception whereas a local variable will.
<!-- bad --><!-- app/views/courses/show.html.erb --><%=render'course_description'%><!-- app/views/courses/_course_description.html.erb --><%=@course.description%><!-- good --><!-- app/views/courses/show.html.erb --><%=render'course_description',course:@course%><!-- app/views/courses/_course_description.html.erb --><%=course.description%>
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 theconfig/locales
directory.
When the labels of an Active Record model need to be translated, use theactiverecord
scope:
en: activerecord: models: user: Member attributes: user: name: 'Full name'
ThenUser.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.
Separate the texts used in the views from translations of Active Record attributes.Place the locale files for the models in a folderlocales/models
and the texts used in the views in folderlocales/views
.
When organization of the locale files is done with additional directories, these directories must be described in theapplication.rb
file in order to be loaded.
# config/application.rbconfig.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 thelocales
directory.
Use the short form of the I18n methods:I18n.t
instead ofI18n.translate
andI18n.l
instead ofI18n.localize
.
Use "lazy" lookup for locale entries from views and controllers. Let’s say we have the following structure:
en: users: show: title: 'User details page'
The value forusers.show.title
can be looked up in the templateapp/views/users/show.html.haml
like this:
# bad=t'users.show.title'# good=t'.title'
Use dot-separated locale keys instead of specifying the:scope
option with an array or a single symbol.Dot-separated notation is easier to read and trace the hierarchy.
# badI18n.t:record_invalid,scope:[:activerecord,:errors,:messages]# goodI18n.t:record_invalid,scope:'activerecord.errors.messages'I18n.t'activerecord.errors.messages.record_invalid'# badI18n.t:title,scope::invitation# goodI18n.t'title.invitation'
More detailed information about the Rails I18n can be found in theRails Guides
Use theasset pipeline to leverage organization within your application.
Reserveapp/assets
for custom stylesheets, javascripts, or images.
Uselib/assets
for your own libraries that don’t really fit into the scope of the application.
When possible, use gemified versions of assets (e.g.jquery-rails,jquery-ui-rails,bootstrap-sass,zurb-foundation).
Name the mailersSomethingMailer
.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.rbconfig.action_mailer.raise_delivery_errors=true
Use a local SMTP server likeMailcatcher in development environment.
# config/environments/development.rbconfig.action_mailer.smtp_settings={address:'localhost',port:1025,# more settings}
Provide default settings for the host name.
# config/environments/development.rbconfig.action_mailer.default_url_options={host:"#{local_ip}:3000"}# config/environments/production.rbconfig.action_mailer.default_url_options={host:'your_site.com'}# in your mailer classdefault_url_options[:host]='your_site.com'
Format the from and to addresses properly.Use the following format:
# in your mailer classdefaultfrom:'Your Name <info@your_site.com>'
If you’re using Rails 6.1 or higher, you can use theemail_address_with_name
method:
# in your mailer classdefaultfrom:email_address_with_name('info@your_site.com','Your Name')
Make sure that the e-mail delivery method for your test environment is set totest
:
# config/environments/test.rbconfig.action_mailer.delivery_method=:test
The delivery method for development and production should besmtp
:
# 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 androadie.
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 ofsidekiq gem.
Prefer Ruby 2.3’s safe navigation operator&.
overActiveSupport#try!
.
# badobj.try!:fly# goodobj&.fly
Prefer Ruby’s Standard Library methods overActiveSupport
aliases.
# 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 Active Support extensions.
# bad(1..50).to_a.forty_two1.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 Active Support’sArray#inquiry
, andString#inquiry
.
# bad - String#inquiryruby='two'.inquiryruby.two?# goodruby='two'ruby =='two'# bad - Array#inquirypets=%w(catdog).inquirypets.gopher?# goodpets=%w(catdog)pets.include?'cat'
Prefer Active Support’sexclude?
over Ruby’s negatedinclude?
.
# bad!array.include?(2)!hash.include?(:key)!string.include?('substring')# goodarray.exclude?(2)hash.exclude?(:key)string.exclude?('substring')
If you’re using Ruby 2.3 or higher, prefer squiggly heredoc (<<~
) over Active Support’sstrip_heredoc
.
# bad<<EOS.strip_heredoc some textEOS# bad<<-EOS.strip_heredoc some textEOS# good<<~EOS some textEOS
If you’re using Rails 7.0 or higher, preferto_fs
overto_formatted_s
.to_formatted_s
is just too cumbersome for a method used that frequently.
# badtime.to_formatted_s(:db)date.to_formatted_s(:db)datetime.to_formatted_s(:db)42.to_formatted_s(:human)# goodtime.to_fs(:db)date.to_fs(:db)datetime.to_fs(:db)42.to_fs(:human)
Configure your timezone accordingly inapplication.rb
.
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 useTime.parse
.
# badTime.parse('2015-03-02 19:05:37')# => Will assume time string given is in the system's time zone.# goodTime.zone.parse('2015-03-02 19:05:37')# => Mon, 02 Mar 2015 19:05:37 EET +02:00
Don’t useString#to_time
# bad - assumes time string given is in the system's time zone.'2015-03-02 19:05:37'.to_time# goodTime.zone.parse('2015-03-02 19:05:37')# => Mon, 02 Mar 2015 19:05:37 EET +02:00
Don’t useTime.now
.
# badTime.now# => Returns system time and ignores your configured time zone.# goodTime.zone.now# => Fri, 12 Mar 2014 22:04:47 EET +02:00Time.current# Same thing but shorter.
Preferall_(day|week|month|quarter|year)
overbeginning_of_(day|week|month|quarter|year)..end_of_(day|week|month|quarter|year)
to get the range of date/time.
# baddate.beginning_of_day..date.end_of_daydate.beginning_of_week..date.end_of_weekdate.beginning_of_month..date.end_of_monthdate.beginning_of_quarter..date.end_of_quarterdate.beginning_of_year..date.end_of_year# gooddate.all_daydate.all_weekdate.all_monthdate.all_quarterdate.all_year
If used without a parameter, preferfrom_now
andago
instead ofsince
,after
,until
orbefore
.
# bad - It's not clear that the qualifier refers to the current time (which is the default parameter)5.hours.since5.hours.after5.hours.before5.hours.until# good5.hours.from_now5.hours.ago
If used with a parameter, prefersince
,after
,until
orbefore
instead offrom_now
andago
.
# bad - It's confusing and misleading to read2.days.from_now(yesterday)2.days.ago(yesterday)# good2.days.since(yesterday)2.days.after(yesterday)2.days.before(yesterday)2.days.until(yesterday)
Avoid using negative numbers for the duration subject. Always prefer using a qualifier that allows using positive literal numbers.
# bad - It's confusing and misleading to read-5.hours.from_now-5.hours.ago# good5.hours.ago5.hours.from_now
Use Duration methods instead of adding and subtracting with the current time.
# badTime.current -1.minuteTime.zone.now +2.days# good1.minute.ago2.days.from_now
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 theGemfile.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
.
Prefer integration style controller tests over functional style controller tests,as recommended in the Rails documentation.
# badclassMyControllerTest <ActionController::TestCaseend# goodclassMyControllerTest <ActionDispatch::IntegrationTestend
PreferActiveSupport::Testing::TimeHelpers#freeze_time overActiveSupport::Testing::TimeHelpers#travel_to with an argument of the current time.
# badtravel_to(Time.now)travel_to(DateTime.now)travel_to(Time.current)travel_to(Time.zone.now)travel_to(Time.now.in_time_zone)travel_to(Time.current.to_time)# goodfreeze_time
There are a few excellent resources on Rails style, that you should consider if you have time to spare:
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 viaPatreon.
It’s easy, just follow the contribution guidelines below:
Make your feature addition or bug fix in a feature branch.
Include agood description of your changes
Push your feature branch to GitHub
Send aPull Request
This work is licensed under aCreative 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