Best practices

Kamil Baćkowski

Databases

Foreign key constraints

create_table :posts do |t|
  t.references :user, foreign_key: true, index: true, null: false
end

Not null constraints

create_table :comments do |t|
  t.text :body, null: false
  t.boolean :published, null: false, default: false # usually we should add not null for boolean fields
end

Nil exceptions

How to properly fix “undefined method `email’ for nil:NilClass”

Problem :

<div>@comment.user.email</div>

Fix :

# migrate the invalid data by either removing comments without user or try to assign comments to i.e. post owner
# ...
change_column_null :comments, :user_id, false

Enum columns

class User < ApplicationRecord
  enum status: [:pending, :approved]
end

Use instead :

class User < ApplicationRecord
  enum status: { pending: 'pending', approved: 'approved' }
end

Avoid useless metaprogramming

class User < ApplicationRecord
  class << self
    %w(pending approved).each do |type|
      define_method("#{type}_only") do
        where(status: type)
      end
    end
  end
end

vs

class User < ApplicationRecord
  def self.pending_only
    where(status: :pending)
  end

  def self.approved_only
    where(status: :approved)
  end
end

ActiveRecord callbacks

class Company < ApplicationRecord
  belongs_to :user
  has_many :roles
  has_one :setting
  has_one :subscription

  after_create :create_roles
  after_create :create_subscription!
  after_create :create_setting!

  def create_roles
    roles.create! user: user, name: :owner
  end
end

vs

class CreateCompany
  def initialize(user)
    @user = user
  end

  def call
    Company.create!(user: @user).tap do |company|
      company.create_setting!
      company.create_subscription!
      company.roles.create! user: @user, name: :owner
    end
  end
end

ActiveRecord callbacks

However callbacks should be used as long as they don’t touch other records or business logic

class Company < ApplicationRecord
  before_create :generate_unique_token
  before_save :strip_name

  def strip_name
    self.name = name.strip
  end

  def generate_unique_token
    self.token = SecureRandom.hex
  end
end

Transactions

def create
  current_user.create_company!.tap do |company|
    company.create_setting!
    company.create_subscription!
    company.roles.create! user: current_user, name: :owner # this raises validation error
  end
end

Result will be a company without role.

Processing by CompaniesController#create as HTML
   (0.1ms)  begin transaction
  SQL (0.9ms)  INSERT INTO "companies" ("user_id") VALUES (?)  [["user_id", 8]]
   (118.3ms)  commit transaction
  Company Load (0.1ms)  SELECT  "companies".* FROM "companies" WHERE "companies"."user_id" = ? LIMIT ?  [["user_id", 8], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "settings" ("company_id") VALUES (?)  [["company_id", 16]]
   (127.3ms)  commit transaction
  Setting Load (0.2ms)  SELECT  "settings".* FROM "settings" WHERE "settings"."company_id" = ? LIMIT ?  [["company_id", 16], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "subscriptions" ("company_id") VALUES (?)  [["company_id", 16]]
   (112.5ms)  commit transaction
  Subscription Load (0.3ms)  SELECT  "subscriptions".* FROM "subscriptions" WHERE "subscriptions"."company_id" = ? LIMIT ?  [["company_id", 16], ["LIMIT", 1]]
   (0.1ms)  begin transaction
   (0.1ms)  rollback transaction

Transactions

def create
  Company.transaction do
    current_user.create_company!.tap do |company|
      company.create_setting!
      company.create_subscription!
      company.roles.create! user: current_user, name: :owner # this raises validation error
    end
  end
end

Or even better:

around_action :with_transaction

def create
  current_user.create_company!.tap do |company|
    company.create_setting!
    company.create_subscription!
    company.roles.create! user: current_user, name: :owner # this raises validation error
  end
end

private

def with_transaction(&block)
  ActiveRecord::Base.transaction(&block)
end

Gems

Do not require gem by default if it’s not needed

gem 'twilio', require: false
# file: lib/send_sms.rb
require 'twilio'

class SendSms
  def self.call(phone_number:, body:)
    #send sms using twilio gem
  end
end

Gems

Think twice before adding gem which functionality can be easily implemented from scratch

Example of gems that should be replaced by own implementation:

  • acts_as_commentable
  • acts-as-taggable-on
  • acts_as_follower

Ruby variables

Name variables according to their type & context:

post
# vs
post_body # because we have string inside
pages
# vs
pages_count # because we have number inside
comment_id
# vs
comment_ids # because we have array inside

RoR tips

user.comments.map(&:id)
# vs
user.comment_ids
comment.user.id
# vs
comment.user_id
user.comments.map { |c| c.body }
# vs
user.comments.map(&:body)
user_admin = UserAdmin.first
# vs
user_admin = UserAdmin.first!

Organize JS

# file: app/assets/javascripts/calendar.coffee
window.Calendar = {
  initialize: ->
    @_initializeDateInputs()
    @_initializeNavigationActions()

  _initializeDateInputs: ->
    @('#calendar .date-inputs').datepicker()
    ...

  _initializeNavigationActions: ->
    ...

# file: app/assets/javascripts/application.js
//= require calendar
<div id="calendar">
  ...
</div>

:coffescript
  Calendar.initialize()

Jquery selectors

$('#sign_up_form').on 'submit', ->
  ...

Create simple exists() function and use it:

$.fn.exists = ->
  if !@length
    throw new Error("No elements matched by " + @selector)
  return @
$('#sign_up_form').exists().on 'submit', ->
  ...

Summary

Questions ?