rails-idioms

SkillAI & models

Rails rewards convention over configuration, Active Record, and RESTful design. Idiomatic Rails = conventional, tested, Hotwire-aware.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the rails-idioms skill

About this capability

Comprehensive sets of standards and practices designed to elevate the capabilities of AI coding agents.

What this skill tells your AI

The instructions your AI receives, as published by irahardianto/awesome-agv in .agents/skills/rails-idioms/SKILL.md and read by ahel’s review.

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:

    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:

    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:

    # ❌ 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:

    # ❌ 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:

    # ❌ 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:

    private
    
    def task_params
      params.require(:task).permit(:title, :priority, :deadline, :description)
    end
    
  3. Service objects for complex business logic:

    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:

    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:

    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:

    <!-- Wraps content that can be independently loaded/replaced -->
    <%= turbo_frame_tag "task_#{task.id}" do %>
      <%= render task %>
    <% end %>
    
  2. Turbo Streams for real-time updates:

    # 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:
    # ❌ 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):

    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:

    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

ToolPurposeCommand
RuboCop + rubocop-railsLintingrubocop --autocorrect
BrakemanSecuritybrakeman --no-pager
bundle auditCVE scanningbundle audit check --update
rails_best_practicesCode qualityrails_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

Signals

GitHub stars
156
Forks
53
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
rails-idioms
Source
github.com/irahardianto/awesome-agv