Ruby & Rails guidelines
We follow the Rubocop official Rubocop Ruby coding style guide as the primary source of best practices and conventions.
1.2. Loading in batches
1.7. Avoid Default Scope
1.9. Check constraints
3.2. Testing
3.2.1 Testing best practices
3.2.1.1 Use Let
3.2.1.2 Use Factories
3.2.1.3 Describe Methods
1.Do's and Don'ts
Add and follow the official MarsBased Rubocop configuration, where most of rules are already defined, highlighting these two:
Use single quotes when possible.
Max length of 90 characters
1.1.Use Dotenv for environment variables
We use the Dotenv gem for managing the environment variables.
1.2.Loading in batches
Don't iterate unlimited / big queries directly. Use find in batches for loading big queries:
# WRONG
Car.all.each do |car|
car.start_engine!
end
# RIGHT
Car.find_each do |car|
car.start_engine!
end1.3.Avoid Active Record callbacks with side effects
Avoid using Active Record callbacks unless it's related to data persistence specially avoiding side effects like sending and e-email.
Consider using the command pattern to send the e-mail from a controller action.
# WRONG
after_save :notify_user
def notify_user
UserMailer.notify(user).deliver
end1.4.Avoid raw SQL queries
Avoid writing raw SQL queries unless strictly necessary.
When using Active Record we have the full power of it. For example if we have a custom serialization for a column, Active Record will automatically convert the value when writing queries.
# WRONG
User.where('active = ?', params[:active])
# RIGHT
User.where(active: params[:active])When writing more complex queries you may use Arel or write the where clause manually. However take into account that if you write it manually you won't have the full power of Active Record, like:
You will not be able to use alias attributes.
You will not be able to use custom types (serialization and deserialization).
class User < ApplicationRecord
alias_attribute :created_at, :dtCreationDate
end
# MANUAL
User.where('dtCreationDate < ?', DateTime.current) # Needs to use column name in the database
# AREL
User.where(User.arel_table[:created_at].lt(DateTime.current)) # Can use aliased name1.5.Size instead of count
Use size instead of count unless you are doing a direct count on a table. Using count always triggers a query while using size is able to use the cached values of a previous query.
# WRONG
Post.published.count
# RIGHT
Post.published.size
# RIGHT
Post.count # Counting directly on the model class1.6.Avoid N+1 Queries with includes
When you have to access an association, avoid N+1 query problems, you can use includes to eager load the associated records:
# WRONG
User.all.each do |user|
user.posts.each do |post|
p post.title
end
end
# RIGHT
User.includes(:posts).each do |user|
user.posts.each do |post|
p post.title
end
endYou can find some more examples in the Active Record guide.
1.7.Avoid Default Scope
In order to avoid unexpected and hidden behaviour, avoid using default_scope and use named scopes and explicit uses of those scopes:
# WRONG
class User < ActiveRecord::Base
default_scope { where(deleted: false) }
end
# RIGHT
class User < ActiveRecord::Base
scope :active, -> { where(deleted: false) }
end1.8.Use find_by for instead where().first
When retrieving a single record from the database, don’t use where(...).first, use find_by instead. And similarly when selecting a single item from a collection use find { ... } instead of select { ... }.first.
# WRONG
User.where(active: true).first
# RIGHT
User.find_by(active: true)1.9.Check database constraints
Check that constraints are correct and that they match the validations. A typical example is adding a default without a not-null constraint.
1.10.Filter sensitive parameters in logs
When receiving parameters in a controller that contain sensitive information like a password or secret key, add the name of the parameter to the list of filtered parameters. Note that :password is already filtered by default.
Rails.application.config.filter_parameters += [:api_key, :secret]2.General project organization and architecture
Follow the standard generated directory structure at project initialization with rails new project_name as described in Ruby On Rails Guide.
Additionally:
Services under the
/app/servicesdirectory.Commands under the
/app/commandsdirectory.Presenters under the
/app/presentersdirectory.Query objects under the
/app/queriesdirectory.Form objects under the
/app/form_objectsdirectory.
2.1.Project structure example
app/
|- assets/
|- channels/
|- controllers/
|- helpers/
|- jobs/
|- mailers/
|- models/
|- form_objects/
|- queries/
|- presenters/
|- services/
|- commands/
|- views/
bin/
config/
|-environments/
|-initializers/
|-locales/
db/
|- migrate/
lib/
|-assets/
|-tasks/
log/
public/
spec/
|- factories/
|- helpers/
|- mailers/
|- models/
|- requests/
|- support/
|- views/
tmp/
vendor/3.Common Patterns
3.1.Devise (Authentication)
Skip all the default routes generated by devise on routes.rb and create custom controllers and views according to the requirements.
routes.rb
routes.rbdevise_for :users, skip: :all
resource :sign_up, only: %i(new create), path_names: { new: '' }
resource :session, only: %i(new create destroy), path: 'login', path_names: { new: '' }
resource :confirmation, only: %i(new create show)
resource :password, only: %i(new create edit update)Sessions Controller example: app/controllers/sessions_controller.rb
app/controllers/sessions_controller.rbclass SessionsController < ApplicationController
prepend_before_action :allow_params_authentication!, only: :create
prepend_before_action :require_no_authentication, only: %i(new create)
def new
@user = User.new
@user.clean_up_passwords
end
def create
@user = authenticate_user!(recall: 'sessions#new')
sign_in(@user)
redirect_to sign_up_path, notice: t('.ok')
end
def destroy
sign_out(:user)
redirect_to new_session_path, notice: t('.ok')
end
endNew session view example: app/views/sessions/new.html.erb
app/views/sessions/new.html.erb<div class="box">
<hgroup class="login__header">
<h1><%= t('.title') %></h1>
</hgroup>
<%= simple_form_for(@user,
url: session_path,
html: { class: 'login__form' }) do |form| %>
<%= form.input :email %>
<%= form.input :password %>
<%= form.submit t('.submit'), class: 'btn-primary is-block' %>
<p class="login__link">
<%= link_to 'Forgot Password?', new_password_path, class: 'link--secondary'%>
</p>
<% end%>
</div>3.2.Testing
We use the Rspec testing framework and usually we write these kind of tests:
Unit tests for models, commands, jobs.
System tests for integration.
Request specs for APIs.
Avoid controller tests: controllers functionality is already covered by integration specs.
3.2.1.Testing Best Practices
3.2.1.1Use let
When you have to assign a variable to test, instead of using a before each block, use let. It is memoized when used multiple times in one example, but not across examples.
describe User do
let(:user) { User.new(name: 'Rocky Balboa') }
it 'has a name' do
expect(user.name).to_not be_nil
end
end3.2.1.2Use factories
Use factory_bot to reduce the verbosity when working with models.
spec/factories/user.rb
FactoryBot.define do
name { 'Rocky Balboa '}
age { 30 }
active { true }
role { :engineer }
endUsing the factory
user = FactoryBot.create(:user, name: 'John Rambo') # The rest of attributes are already set3.2.1.3Describe Methods
When testing a method, create a describe block with the name of the method and place the specs inside. Use "." as prefix for class methods and "#" as prefix for instance methods.
describe ".authenticate" do
it 'returns true when the user is active' { ... }
it 'returns false when the user is deleted' { ... }
end
describe "#generate_export" do
it 'returns an empty array when there are not users' { ... }
it 'returns the list of active users' { ... }
end4.Gems
General
Keynote for presenters.
Simple form for form generation.
Dotenv + dotenv-rails for environment variables.
Activeadmin for admin panels.
Devise for authentication.
Sidekiq for background jobs.
Sidekiq-cron for scheduled jobs.
Sidekiq-failures for error tracking in background jobs.
Shrine for file uploads.
Http (http-rb) for http calls.
Friendly_id for slugged url generation.
Kaminari for pagination.
Jbuilder for JSON API responses.
Pundit for authorization.
Testing:
Rspec testing framework.
FactoryBot for factories.
Webmock (not VCR) to mock external HTTP requests.
Capybara for integration tests.
Dev
Better_errors for error enhancements.
Pry + pry-rails for a better console.
Pry-byebug for console debugging.
Bullet to detect N+1 queries.
PgHero for database insights.
Last updated
