Ruby on Rails

SE II Ref
Theme
↑
Finders
# Basic
User.find(1)              # raises if missing
User.find_by(email: 'x')  # nil if missing
User.find_by!(email: 'x') # raises if missing
User.first / User.last
User.all                   # relation, not array

# Dynamic finders
User.find_by_email('x')   # deprecated but asked

# Find or create
User.find_or_create_by(email: 'x')
User.find_or_initialize_by(email: 'x')
Querying
# Conditions
User.where(active: true)
User.where("age > ?", 18)       # safe
User.where("age > #{age}")       # ⚠ SQL injection!
User.where.not(banned: true)
User.where(role: ['admin', 'mod']) # IN

# Order, limit, offset
User.order('created_at DESC')
User.order(created_at: :desc)
User.limit(10).offset(20)

# Aggregates
User.count / .sum(:score) / .average(:age)
User.minimum(:age) / .maximum(:age)
Associations & Eager Loading
# N+1 killer
Post.includes(:author, :comments)
Post.eager_load(:author)   # LEFT OUTER JOIN
Post.preload(:author)      # separate query

# Joining (for filtering only)
Post.joins(:comments).where(comments: { approved: true })

# Nested includes
Post.includes(comments: :author)

# Association queries
user.posts.where(published: true)
user.posts.create(title: 'Hi')
user.posts.build(title: 'Hi')  # unsaved
⚠ includes loads data; joins filters only β€” don't swap them.
Scopes
class Post < ApplicationRecord
  scope :published, -> { where(published: true) }
  scope :recent,    -> { order(created_at: :desc) }
  scope :by_author, ->(id) { where(author_id: id) }

  # Default scope (use sparingly)
  default_scope { where(deleted_at: nil) }
end

# Chainable
Post.published.recent.limit(5)
Post.by_author(42).published

# Unscope default
Post.unscoped.where(id: 1)
Create / Update / Delete
# Create
User.create(name: 'Eric')      # save + return
User.create!(name: 'Eric')     # raises on invalid
user = User.new(name: 'Eric')
user.save                       # true/false
user.save!                      # raises

# Update
user.update(name: 'New')        # runs callbacks
user.update_column(:name, 'x') # skips callbacks!
User.update_all(active: false) # no callbacks

# Delete
user.destroy                    # runs callbacks
user.delete                     # skips callbacks
User.destroy_all(active: false)
User.where(active: false).delete_all
Transactions & Batching
# Transaction
ActiveRecord::Base.transaction do
  account.debit(100)
  other.credit(100)
  raise ActiveRecord::Rollback # silent rollback
end

# Batch processing (memory-safe)
User.find_each(batch_size: 500) do |user|
  user.do_something
end

User.find_in_batches(batch_size: 500) do |batch|
  process(batch)
end
πŸ’‘ find_each ignores order β€” don't chain .order()
Resources & REST
# Full REST (7 routes)
resources :posts

# Limit actions
resources :posts, only: [:index, :show, :create]
resources :posts, except: [:destroy]

# Singular resource (no :id)
resource  :profile  # show/edit/update/destroy

# Nested
resources :posts do
  resources :comments, only: [:create, :destroy]
end
# β†’ /posts/:post_id/comments
Extra Actions & Namespaces
# Member (has :id) vs Collection (no :id)
resources :posts do
  member     { post :publish }  # /posts/:id/publish
  collection { get  :search }  # /posts/search
end

# Namespace (module + path prefix)
namespace :api do
  namespace :v1 do
    resources :users
  end
end
# β†’ /api/v1/users β†’ Api::V1::UsersController

# Scope (path only, no module)
scope '/api/v1' do
  resources :users
end
Named Routes & Helpers
# Auto-generated path/url helpers
posts_path              # /posts
post_path(@post)        # /posts/1
new_post_path           # /posts/new
edit_post_path(@post)   # /posts/1/edit
posts_url               # http://... full URL

# Nested
post_comments_path(@post)
post_comment_path(@post, @comment)

# Custom named route
get 'sign_in', to: 'sessions#new', as: :login
# β†’ login_path
Constraints & Concerns
# Constraints
get 'posts/:id', to: 'posts#show',
  constraints: { id: /\d+/ }

# Route concern (DRY)
concern :commentable do
  resources :comments
end

resources :posts,    concerns: :commentable
resources :articles, concerns: :commentable

# Root & redirect
root 'home#index'
get '/old', to: redirect('/new')
7 REST Routes at a Glance
VerbPathActionHelperUse
GET/postsindexposts_pathList all
GET/posts/newnewnew_post_pathNew form
POST/postscreateposts_pathCreate
GET/posts/:idshowpost_path(@p)Show one
GET/posts/:id/editeditedit_post_path(@p)Edit form
PATCH/PUT/posts/:idupdatepost_path(@p)Update
DELETE/posts/:iddestroypost_path(@p)Delete
Structure & Filters
class PostsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  def index
    @posts = Post.all
  end

  def create
    @post = Post.new(post_params)
    if @post.save
      redirect_to @post, notice: 'Created!'
    else
      render :new, status: :unprocessable_entity
    end
  end

  private
  def set_post;    @post = Post.find(params[:id]); end
  def post_params; params.require(:post).permit(:title, :body); end
end
Strong Params
# require + permit (whitelist)
params.require(:user).permit(:name, :email)

# Nested attributes
params.require(:post).permit(
  :title,
  tags: [],
  comments_attributes: [:body, :_destroy]
)

# Optional require
params.permit(:search)

# ⚠ Never pass raw params to AR:
# User.new(params[:user])  ← mass assignment!
Filter Types
before_action              # before action runs
after_action               # after (even on error)
around_action              # wraps with yield
skip_before_action :auth, only: :index

# Halting: render/redirect stops chain
def check_admin
  redirect_to root_path unless current_user.admin?
end

# Respond to format
respond_to do |format|
  format.html
  format.json { render json: @post }
end
Render & Redirect
# Render (same request)
render :new
render json: { error: 'bad' }, status: :unprocessable_entity
render plain: 'OK'
head :no_content           # 204, no body

# Redirect (new request)
redirect_to posts_path
redirect_to @post
redirect_to root_path, notice: 'Done'
redirect_back fallback_location: root_path
⚠ render + redirect in same action = DoubleRenderError. Use return after redirect.
params / session / flash
# params
params[:id]            # route segment
params[:q]             # query string
params[:post][:title]  # form data

# session (cookie-backed)
session[:user_id] = user.id
session.delete(:user_id)

# flash (persists one request)
flash[:notice] = 'Saved!'
flash[:alert]  = 'Error!'
flash.now[:alert] = 'Now only' # current req only
rescue_from & HTTP Status
rescue_from ActiveRecord::RecordNotFound,
  with: :not_found
rescue_from Pundit::NotAuthorized do
  render json: { error: 'Forbidden' }, status: 403
end

def not_found
  render json: { error: 'Not found' }, status: 404
end

# Common status symbols
# :ok 200  :created 201  :no_content 204
# :bad_request 400  :unauthorized 401
# :forbidden 403  :not_found 404
# :unprocessable_entity 422
API Controller Setup
class Api::V1::BaseController < ActionController::API
  before_action :authenticate!
  rescue_from ActiveRecord::RecordNotFound do
    render json: { error: 'Not found' }, status: 404
  end

  private
  def authenticate!
    token = request.headers['Authorization']
              &.split(' ')&.last
    @current_user = User.find_by!(token: token)
  end
end

# ActionController::API = no view layer
# Use ::Base if you need sessions/views
render json Patterns
# Simple
render json: @user
render json: @user, status: :created

# Only / except
render json: @user, only: [:id, :name]
render json: @user, except: [:password_digest]

# Include associations
render json: @post,
  include: { comments: { only: [:id, :body] } }

# Envelope + meta
render json: {
  data: @users,
  meta: { total: @users.count, page: params[:page] }
}
Common HTTP Status Codes
CodeRailsMeaningUse
200:okOKGET success / generic success
201:createdCreatedPOST created a resource
204:no_contentNo ContentSuccess with empty body (DELETE, PUT)
400:bad_requestBad RequestMalformed params / invalid JSON
401:unauthorizedUnauthorizedMissing/invalid auth credentials
403:forbiddenForbiddenAuthenticated but not allowed
404:not_foundNot FoundResource missing / wrong ID
409:conflictConflictState conflict (e.g., duplicate, versioning)
422:unprocessable_entityUnprocessable EntityValidation failures
429:too_many_requestsToo Many RequestsRate limiting
500:internal_server_errorServer ErrorUnexpected exception
If you can’t decide: use 400 for malformed input, 422 for valid input that fails validations.
as_json / to_json
def as_json(options = {})
  super(options.merge(
    only: [:id, :name, :email],
    methods: [:full_name],
    include: { role: { only: :name } }
  ))
end

# methods: adds virtual attr to output
def full_name
  "#{first_name} #{last_name}"
end

# to_json calls as_json under the hood
# render json: @obj calls as_json auto
AMS & Jbuilder
# AMS (app/serializers/user_serializer.rb)
class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :email
  has_many :posts

  def email
    object.email.downcase
  end
end
render json: @user  # auto-uses serializer

# Jbuilder (show.json.jbuilder)
json.(@user, :id, :name)
json.posts(@user.posts) do |post|
  json.(post, :id, :title)
end
Pagination & Headers
# Kaminari
@posts = Post.page(params[:page]).per(25)
@posts.total_count  /  @posts.current_page

# will_paginate
@posts = Post.paginate(page: params[:page], per_page: 25)

# Response headers
response.headers['X-Total-Count'] = @posts.total_count.to_s
response.headers['X-Page']        = params[:page]

# Content
request.format  # :html / :json / :xml
request.xhr?    # AJAX?
CORS & Versioning
# rack-cors (config/initializers/cors.rb)
Rails.application.config.middleware
  .insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any,
      methods: [:get, :post, :patch, :delete]
  end
end

# Versioning strategies
# 1. URL:      /api/v1/posts         ← most common
# 2. Header:   Accept: application/vnd.v1+json
# 3. Subdomain: api.v1.myapp.com
Validations
class User < ApplicationRecord
  validates :name,  presence: true,
                     length: { minimum: 2, maximum: 50 }
  validates :email, presence: true,
                     uniqueness: { case_sensitive: false },
                     format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :age,   numericality: { greater_than: 0 }
  validates :role,  inclusion: { in: %w[admin user guest] }
  validates :bio,   length: { maximum: 500 }, allow_blank: true
  validate  :custom_check

  def custom_check
    errors.add(:base, 'bad') if bad?
  end
end
Associations
class Post < ApplicationRecord
  belongs_to :user
  belongs_to :category, optional: true
  has_many   :comments,   dependent: :destroy
  has_many   :commenters, through: :comments, source: :user
  has_one    :image,       dependent: :destroy
  has_many   :tags,        through: :post_tags
end

# dependent: options
# :destroy      β†’ calls destroy (callbacks run)
# :delete_all   β†’ SQL DELETE, no callbacks
# :nullify      β†’ sets FK to null
# :restrict_with_error β†’ blocks destroy
Callbacks
before_validation / after_validation
before_save        # create AND update
before_create / after_create
after_save
before_update / after_update
before_destroy / after_destroy
after_commit       # after DB tx commits ← use for jobs
after_rollback

# Examples
before_save  :normalize_email
after_create :send_welcome_email
after_commit :enqueue_job, on: :create
πŸ’‘ Use after_commit for jobs/mailers β€” only fires if transaction succeeds.
Migrations
# Generate
rails g migration AddSlugToPosts slug:string:uniq
rails g migration CreatePosts title:string user:references

# Methods
add_column    :posts, :slug, :string, null: false, default: ''
remove_column :posts, :old_field
rename_column :posts, :old, :new_name
change_column :posts, :views, :bigint
add_index     :users, :email, unique: true
add_reference :posts, :author,
  foreign_key: { to_table: :users }

# Types: string text integer bigint float
#        decimal boolean datetime date jsonb
Errors & Checking Validity
# After save attempt
user.valid?                # runs validations, returns bool
user.invalid?
user.errors.full_messages  # ["Name can't be blank"]
user.errors[:email]        # ["is invalid"]
user.errors.any?  /  user.errors.count

# In controller β€” respond after failed save
if @user.save
  render json: @user, status: :created
else
  render json: { errors: @user.errors.full_messages },
         status: :unprocessable_entity
end
Service Objects
class CreatePost
  def initialize(user:, params:)
    @user, @params = user, params
  end

  def call
    post = @user.posts.build(@params)
    post.save!
    NotifyJob.perform_later(post)
    post
  end
end

# In controller
@post = CreatePost.new(
  user: current_user,
  params: post_params
).call
Active Job
class WelcomeEmailJob < ApplicationJob
  queue_as :default

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver_now
  end
end

# Enqueue
WelcomeEmailJob.perform_later(user.id)
WelcomeEmailJob.set(wait: 5.minutes)
             .perform_later(user.id)

# ⚠ Pass IDs not objects
# Backends: Sidekiq, Delayed::Job, Resque
Caching
# Low-level
Rails.cache.fetch("user/#{id}/stats", expires_in: 12.hours) do
  User.find(id).compute_stats
end
Rails.cache.write('key', val)
Rails.cache.read('key')
Rails.cache.delete('key')

# HTTP caching
expires_in 1.hour, public: true
fresh_when @post   # sets ETag + Last-Modified
stale?(@post)       # check & set headers
Concerns
module Trackable
  extend ActiveSupport::Concern

  included do
    before_create :set_uuid
    scope :recent, -> { order(created_at: :desc) }
  end

  def set_uuid
    self.uuid = SecureRandom.uuid
  end

  class_methods do
    def find_by_uuid(uuid) = find_by!(uuid: uuid)
  end
end

class Post < ApplicationRecord
  include Trackable
end
Ruby Idioms
# Safe navigation
user&.profile&.avatar_url

# Memoize
@result ||= expensive_call

# Presence / blank
"  ".blank?    # true (whitespace = blank)
"".present?   # false
nil.presence  # nil  (returns nil if blank)
"x".presence  # "x"

# Shortcuts
[1,2,3].map(&:to_s)     # symbol to proc
%w[foo bar baz]          # word array
%i[foo bar baz]          # symbol array
hash.fetch(:key, default)
Environment & Config
# Env checks
Rails.env.production?
Rails.env.development?
Rails.env.test?

# Credentials (Rails 5.2+)
Rails.application.credentials.secret_key
Rails.application.credentials.dig(:aws, :key)

# ENV vars
ENV.fetch('DATABASE_URL')        # raises if missing
ENV.fetch('PORT', '3000')       # with default

# Route helpers outside views
include Rails.application.routes.url_helpers