skills/catalog/ruby/ruby-rails/SKILL.md
--- name: ruby-rails description: Use for Rails apps: ActiveRecord/models, migrations, controllers/routes, background jobs, caching, and Hotwire. Not for non-Rails Ruby. --- # Ruby on Rails Idiomatic Rails development — conventions, ActiveRecord, and production patterns. ## Workflow 1. **Model** — schema + migrations + validations + scopes 2. **Controller** — thin controllers, strong params, respond_to 3. **Background** — ActiveJob with Sidekiq, idempotent + retry-safe 4. **Performance** — N
npx skillsauth add erikstmartin/dotfiles skills/catalog/ruby/ruby-railsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Idiomatic Rails development — conventions, ActiveRecord, and production patterns.
class AddEmailVerifiedToUsers < ActiveRecord::Migration[7.1]
def change
# Step 1: nullable column (safe to deploy before backfill)
add_column :users, :email_verified, :boolean
# Step 2: backfill in batches to avoid full-table lock
User.in_batches.update_all(email_verified: false)
# Step 3: enforce the constraint after backfill completes
change_column_null :users, :email_verified, false, false
change_column_default :users, :email_verified, false
end
end
Always add indexes alongside foreign keys and query predicates. Use add_index :orders, :customer_id in the same migration as the column.
class Order < ApplicationRecord
belongs_to :customer
has_many :items, dependent: :destroy
# Composable scopes — chain freely without N+1
scope :recent, -> { order(created_at: :desc) }
scope :for_customer, ->(id) { where(customer_id: id) }
scope :pending, -> { where(status: :pending) }
scope :with_details, -> { includes(:items, :customer) }
# Enum for finite states — generates predicates like order.pending?
enum :status, { pending: 0, confirmed: 1, shipped: 2, delivered: 3 }
validates :customer, presence: true
validates :status, presence: true
end
Controller stays thin — it composes scopes, it doesn't implement business logic:
class OrdersController < ApplicationController
def index
@orders = Order.with_details.recent.for_customer(params[:customer_id])
end
def create
@order = Order.new(order_params)
if @order.save
redirect_to @order, notice: "Order created"
else
render :new, status: :unprocessable_entity
end
end
private
def order_params
params.require(:order).permit(:customer_id, :notes, items_attributes: [:product_id, :quantity])
end
end
class ProcessOrderJob < ApplicationJob
queue_as :default
retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
discard_on ActiveRecord::RecordNotFound
def perform(order_id)
order = Order.find_by(id: order_id)
return unless order&.pending? # Idempotent guard — safe to re-run
ActiveRecord::Base.transaction do
order.confirm!
InventoryService.reserve(order)
end
OrderMailer.confirmation(order).deliver_later
end
end
Pass IDs, not full objects — Active Job serializes records by ID and re-loads on execution to avoid stale data.
# Fragment caching with automatic expiry via cache_key_with_version
<% cache @order do %>
<%= render @order %>
<% end %>
# Russian-doll: parent key changes when child changes
<% cache [@customer, @customer.orders.maximum(:updated_at)] do %>
<% @customer.orders.each do |order| %>
<% cache order do %><%= render order %><% end %>
<% end %>
<% end %>
For low-traffic read-heavy data, use Rails.cache.fetch:
def self.featured
Rails.cache.fetch("products/featured", expires_in: 1.hour) do
where(featured: true).with_details.to_a
end
end
includes (or preload/eager_load) for associations rendered in views or serializers; use Bullet gem to catch them in developmentadd_index :users, :email, unique: true and foreign key constraints at the DB leveldependent: :destroy on large associations triggers callbacks for every record — use dependent: :delete_all or schedule a background cleanup job for tables with millions of rowsupdate_all and delete_all skip callbacks and validations — use find_each { |r| r.update(...) } when callbacks matter, but be aware of the performance tradeoff{ "_aj_globalid": "gid://..." } and re-query on load; if the record is deleted before the job runs, it raises unless you discard_on ActiveRecord::RecordNotFoundtesting
Use when creating new skills, editing existing skills, or verifying skills work before deployment
development
Use when you have a spec or requirements for a multi-step task, before touching code
data-ai
Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
tools
Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions