irahardianto / rails-idioms
Ruby on Rails framework patterns: ActiveRecord scopes, N+1 query prevention, model validations, RESTful routing, and RSpec/Minitest testing. Use when building, maintaining, or auditing Rails web applications and APIs. Pair with ruby-idioms.
Rails Idioms and Patterns
Rails rewards convention over configuration, Active Record, and RESTful design. Idiomatic Rails = conventional, tested, Hotwire-aware.
Scope: Rails-specific patterns. For Ruby:
@.agents/skills/ruby-idioms/SKILL.md.
Active Record
Scopes for reusable queries:
`ruby
class Task < ApplicationRecord
scope :active, -> { where(status: :active) }
scope :by_priority, -> { order(priority: :desc) }
scope :createdafter, ->(date) { where('createdat > ?', date) }# ✅ Chainable
# Task.active.bypriority.createdafter(1.week.ago)
end`Validations in models, not controllers:
`ruby
class Task < ApplicationRecord
validates :title, presence: true, length: { maximum: 200 }
validates :priority, inclusion: { in: %w[low medium high] }# ✅ Custom validation
validate :deadlinemustbeinfuture, on: :createprivate
def deadlinemustbeinfuture
return unless deadline.present? && deadline < Time.current
errors.add(:deadline, 'must be in the future')
end
end`includes/preloadfor eager loading — avoid N+1:`ruby
# ❌ N+1 — fires a query per task to load user
Task.all.each { |t| puts t.user.name }# ✅ Eager load — 2 queries total
Task.includes(:user).each { |t| puts t.user.name }# ✅ Use Bullet gem to detect N+1 in development
`Callbacks — use sparingly, prefer service objects:
`ruby
# ❌ Complex callback chains — hard to test and debug
beforesave :normalizetitle, :setdefaults, :notifyassignee# ✅ Service object — explicit, testable
class CreateTask
def call(params)
task = Task.new(normalize(params))
task.save!
NotificationService.notify(task.assignee, task)
task
end
end`
Controllers
RESTful actions — only standard 7 actions per controller. Custom actions = new controller:
`ruby
# ❌ Custom action crammed into TasksController
def complete; end# ✅ Dedicated controller
class TaskCompletionsController < ApplicationController
def create
task = Task.find(params[:task_id])
task.complete!
redirect_to task
end
end`Strong parameters — never mass-assign without permit:
`ruby
privatedef task_params
params.require(:task).permit(:title, :priority, :deadline, :description)
end`Service objects for complex business logic:
`ruby
class TasksController < ApplicationController
def create
result = CreateTask.new.call(task_params)
if result.success?
redirect_to result.task, notice: 'Task created'
else
@task = result.task
render :new, status: :unprocessable_entity
end
end
end`
Error Handling
For universal error handling principles, see
.agents/rules/error-handling-principles.md.
rescue_fromfor controller-level error handling:`ruby
class ApplicationController < ActionController::Base
rescuefrom ActiveRecord::RecordNotFound, with: :notfound
rescue_from ActiveRecord::RecordInvalid, with: :unprocessableprivate
def not_found(exception)
render json: { error: exception.message }, status: :not_found
enddef unprocessable(exception)
render json: { errors: exception.record.errors.full_messages },
status: :unprocessable_entity
end
end`Custom domain errors:
`ruby
module TaskErrors
class NotAssignable < StandardError; end
class DeadlinePassed < StandardError; end
end`- Never rescue
Exception— always rescueStandardErroror specific subclasses.
Hotwire (7+)
Turbo Frames for partial page updates:
`erb
<!-- Wraps content that can be independently loaded/replaced -->
<%= turboframetag "task_#{task.id}" do %>
<%= render task %>
<% end %>`Turbo Streams for real-time updates:
`ruby
# In controller — auto-broadcasts updates
def create
@task = Task.create!(task_params)
respond_to do |format|
format.turbo_stream
format.html { redirectto taskspath }
end
end`- Stimulus for JavaScript sprinkles — minimal JS.
Security
For universal security principles, see
.agents/rules/security-principles.md.
- CSRF protection — enabled by default, never disable
- Content Security Policy — configure in
config/initializers/content_security_policy.rb Parameterized queries — Active Record handles this, but never use string interpolation in
where:# ❌ SQL injection risk Task.where("title LIKE '%#{params[:q]}%'") # ✅ Parameterized Task.where("title LIKE ?", "%#{params[:q]}%")
Anti-Patterns
- ❌ Fat models — extract to service objects / form objects / query objects
- ❌ Business logic in controllers — controllers are thin routing layers
- ❌ Callbacks for complex side effects — use service objects
- ❌
default_scope— implicit, surprising, hard to override - ❌
update_attribute(skips validation) — useupdate! - ❌
rescue Exception— catches everything includingSystemExit,Interrupt - ❌ String interpolation in SQL — SQL injection vector
Testing
For universal testing principles, see
.agents/rules/testing-strategy.md. Below: language-specific patterns only.
RSpec (preferred):
`ruby
RSpec.describe TasksController, type: :request do
describe 'POST /tasks' do
it 'creates a task with valid params' do
post tasks_path, params: { task: { title: 'Test', priority: 'high' } }
expect(response).to havehttpstatus(:created)
expect(Task.last.title).to eq('Test')
endit 'returns errors with invalid params' do
post tasks_path, params: { task: { title: '' } }
expect(response).to havehttpstatus(:unprocessable_entity)
end
end
end`FactoryBot for test data:
`ruby
FactoryBot.define do
factory :task do
title { Faker::Lorem.sentence(word_count: 3) }
priority { %w[low medium high].sample }
association :user
end
end`- Database Cleaner for test isolation.
- Shoulda Matchers for model spec shortcuts.
Formatting and Static Analysis
| Tool | Purpose | Command |
|---|---|---|
| RuboCop + rubocop-rails | Linting | rubocop --autocorrect |
| Brakeman | Security | brakeman --no-pager |
bundle audit | CVE scanning | bundle audit check --update |
rails_best_practices | Code quality | rails_best_practices . |
Related
- Ruby Idioms @.agents/skills/ruby-idioms/SKILL.md
- Database Design Principles @.agents/rules/database-design-principles.md
- Security Principles @.agents/rules/security-principles.md
- API Design Principles @.agents/rules/api-design-principles.md