rails-pro
Ruby on Rails guidance — MVC conventions, ActiveRecord mastery, Hotwire, background jobs, and production ops.
SKILL.mdskills/rails-pro/SKILL.md
GitHubOverview
Rails is the original convention-over-configuration framework: follow its opinions about naming, directories, and REST, and enormous amounts of behavior (routing, ORM, migrations, mailers, jobs, caching) come free. Deviate silently and you fight the framework at every step. This skill covers idiomatic Rails — the parts that make teams fast (ActiveRecord, migrations, Hotwire) and the parts that bite (N+1s, callback spaghetti, deploy config) — plus modern Rails with import maps, Turbo, and Stimulus.
When to use
- Building a full-stack web app the Rails way (monolith-first).
- Fixing N+1 queries, slow pages, or callback-heavy models.
- Structuring jobs, mailers, and service objects in a growing app.
- Using Hotwire (Turbo + Stimulus) instead of a JS SPA.
- Deploying Rails to production (assets, DB, workers, caching).
- Upgrading Rails across major versions.
- Adding an API to an existing Rails monolith.
Core concepts
- Convention over configuration. File names, class names, table names, and routes map to each other (
UsersController→app/controllers/users_controller.rb→userstable). Learn the conventions once and the framework disappears. - ActiveRecord is the heart. Models map to tables; associations (
has_many,belongs_to) generate queries. Eager load with.includesto kill N+1s; use scopes for composable query logic; keep SQL fragments minimal and parameterized. - Migrations with discipline. Each migration should be small, reversible (
change), and safe on large tables — add columns without defaults/backfill separately, add indexesCONCURRENTLY(viaalgorithm: :concurrentlyon Postgres), never rewrite history that's deployed. - Callbacks are a code smell at scale.
before_save/after_commitchains create invisible coupling. Prefer explicit service objects or form objects for multi-model operations; reserve callbacks for true model invariants. - Hotwire over SPA. Turbo Drive/Frames/Streams plus Stimulus controllers deliver SPA-like interactivity with server-rendered HTML. Reach for it before adding React — most CRUD apps don't need a JS framework.
- Background jobs via ActiveJob. Sidekiq (Redis-backed) is the standard adapter. Jobs must be idempotent and retry-safe; pass IDs, not objects; keep payloads small.
- The asset pipeline, modern. Import maps + Sprockets or Propshaft for JS without a bundler; use a bundler (esbuild/vite via jsbundling-rails) only when you need npm-heavy frontend code.
- Strong Parameters.
params.require(:user).permit(:name, :email)— the mass-assignment protection at the controller boundary. Never skip it; it's the difference between a form and a privilege-escalation vector. - Caching. Russian-doll fragment caching, low-level
Rails.cache.fetch, HTTP caching with ETags — Rails' caching is excellent when keys are versioned on the underlying records (cache_key_with_version). - Zeitwerk autoloading. Constants map to file paths; no more
requirespaghetti. Name files correctly and autoloading disappears as a concern — misname them and you get uninitialized-constant mysteries. - ActiveStorage. Uploads backed by local disk/S3/GCS with variants for image processing; use direct uploads so large files bypass the app server.
- Action Mailbox/Text. Inbound email/SMS routed into the app as first-class objects — handy for support inboxes and reply-by-email features without extra services.
Practical workflow
Generate with intent.
rails new shop --database=postgresql --css=tailwind; keep the monolith until a real boundary demands extraction.Model the domain. Resources first (
resources :orders), models with validations and associations, scopes for query logic, service objects (app/services/) for multi-step operations.class Order < ApplicationRecord belongs_to :customer has_many :line_items, dependent: :destroy scope :recent, -> { order(created_at: :desc) } scope :paid, -> { where(status: "paid") } endKill N+1s early. Install the
bulletgem in development — it shouts when you eager-load wrong. Review withrack-mini-profileron slow pages.Add interactivity with Hotwire. Turbo Frames for partial updates, Turbo Streams for realtime via ActionCable, Stimulus controllers for client-side behavior. Write system tests for the flows.
- Keep Stimulus controllers small and single-purpose; reach for Turbo Streams before custom JS.
Jobs and mailers. ActiveJob + Sidekiq; mailers with parameterized previews; all async work idempotent with exponential backoff.
class InvoiceJob < ApplicationJob queue_as :billing retry_on ActiveRecord::Deadlocked, attempts: 5 def perform(invoice_id) Invoice.find(invoice_id).generate_pdf! end endTest the pyramid. Model/unit tests fast and many; request specs for API behavior; system tests (Cuprite/Selenium) sparingly for critical flows — they're slow, so choose the flows that matter.
Cache deliberately. Fragment caching with versioned keys,
stale?/fresh_whenfor HTTP caching, and low-level caching for expensive computations — with explicit expiration strategies.Ship it. Compile assets at build time, run migrations in a release phase (not in web boot), provision Redis for cache/jobs/Cable, set
RAILS_MASTER_KEYvia secrets management, and enable structured logging.# Turbo Stream broadcast from a model — realtime without custom JS class Message < ApplicationRecord after_create_commit -> { broadcast_append_to "room_#{room_id}", target: "messages" } end
Common pitfalls
- N+1 queries in views and serializers — Bullet in dev,
includesin code, query-count assertions in tests. - Callback hell — chains of
after_savethat trigger other saves; extract to service objects with explicit sequencing. - Unsafe migrations on big tables — locking
ALTER TABLEduring deploys; use concurrent index creation and multi-step column changes. - Passing ActiveRecord objects to jobs — serialization breaks and stale data; pass IDs and re-fetch.
- Ignoring the asset pipeline — shipping uncompiled assets or massive JS bundles; audit with build output sizes.
- Secrets in the repo —
credentials.yml.enc+ master key via env/secret store; never commit the key. - No database timeouts — set
statement_timeoutand pool sizes; a hung query should fail, not cascade. - Skipping Strong Parameters — mass-assignment vulnerabilities; permit explicitly at every controller boundary.
- ActionCable without Redis in production — the async adapter doesn't work across processes; configure the Redis adapter.
- Forgetting
dependent:on associations — orphaned rows accumulating; decide destroy/nullify/restrict explicitly per association. - Turbo + custom JS conflicts — event listeners vanish on Turbo navigation; hook
turbo:loador move behavior into Stimulus controllers. - Abusing
default_scope— surprising query behavior in every context; prefer explicit named scopes at call sites. - CookieStore overflow — sessions over 4KB break; switch the session store to cache or DB when storing more than a user id.