--- name: rails-idioms description: >- 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 1. **Scopes for reusable queries:** ```ruby class Task < ApplicationRecord scope :active, -> { where(status: :active) } scope :by_priority, -> { order(priority: :desc) } scope :created_after, ->(date) { where('created_at > ?', date) } # ✅ Chainable # Task.active.by_priority.created_after(1.week.ago) end ``` 2. **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 :deadline_must_be_in_future, on: :create private def deadline_must_be_in_future return unless deadline.present? && deadline < Time.current errors.add(:deadline, 'must be in the future') end end ``` 3. **`includes`/`preload`** for 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 ``` 4. **Callbacks — use sparingly, prefer service objects:** ```ruby # ❌ Complex callback chains — hard to test and debug before_save :normalize_title, :set_defaults, :notify_assignee # ✅ 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 1. **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 ``` 2. **Strong parameters** — never mass-assign without permit: ```ruby private def task_params params.require(:task).permit(:title, :priority, :deadline, :description) end ``` 3. **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`. 1. **`rescue_from` for controller-level error handling:** ```ruby class ApplicationController < ActionController::Base rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActiveRecord::RecordInvalid, with: :unprocessable private def not_found(exception) render json: { error: exception.message }, status: :not_found end def unprocessable(exception) render json: { errors: exception.record.errors.full_messages }, status: :unprocessable_entity end end ``` 2. **Custom domain errors:** ```ruby module TaskErrors class NotAssignable < StandardError; end class DeadlinePassed < StandardError; end end ``` 3. **Never rescue `Exception`** — always rescue `StandardError` or specific subclasses. ### Hotwire (7+) 1. **Turbo Frames** for partial page updates: ```erb <%= turbo_frame_tag "task_#{task.id}" do %> <%= render task %> <% end %> ``` 2. **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 { redirect_to tasks_path } end end ``` 3. **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`: ```ruby # ❌ 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)** — use `update!` - ❌ **`rescue Exception`** — catches everything including `SystemExit`, `Interrupt` - ❌ **String interpolation in SQL** — SQL injection vector ### Testing > For universal testing principles, see `.agents/rules/testing-strategy.md`. Below: language-specific patterns only. 1. **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 have_http_status(:created) expect(Task.last.title).to eq('Test') end it 'returns errors with invalid params' do post tasks_path, params: { task: { title: '' } } expect(response).to have_http_status(:unprocessable_entity) end end end ``` 2. **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 ``` 3. **Database Cleaner** for test isolation. 4. **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