Ruby on Rails Development
When to use
- Scaffolding or extending a Rails 7+ application (full-stack, API-only, or hybrid)
- Designing ActiveRecord models, associations, validations, and callbacks
- Writing database migrations and managing schema changes
- Implementing background jobs with Sidekiq and ActiveJob
- Building reactive UIs with Hotwire (Turbo Frames, Turbo Streams, Stimulus)
- Hardening Rails security defaults (CSRF, CSP, mass-assignment, host authorisation)
- Testing with RSpec + FactoryBot or Minitest
Workflow
- Scaffold with
rails new my-app(full stack) orrails new my-api --api(API-only). For API-only apps add--skip-asset-pipeline --skip-javascript. Use Ruby 3.3+ and Rails 7.2+. - Structure the application:
Keep controllers thin — one controller action should: permit params → call one service object → render/redirect.app/ controllers/ ← Thin: permit params, call service/command, respond models/ ← ActiveRecord: associations, validations, scopes services/ ← Business logic POROs (Plain Old Ruby Objects) jobs/ ← ActiveJob / Sidekiq jobs views/ ← ERB templates + Turbo partials components/ ← ViewComponent for complex UI components (optional) - Define the data model — see
.claude/skills/data-modeling/SKILL.md. Generate migrations withrails generate migration. Follow theYYYYMMDDHHMMSS_verb_noun_on_table.rbnaming convention. - ActiveRecord models:
- Declare
belongs_to,has_many,has_one,has_and_belongs_to_manyrelationships explicitly. - Add database-level constraints AND ActiveRecord validations — they are complementary, not redundant.
- Use named scopes (
scope :active, -> { where(status: 'active') }) for reusable query fragments. - Callbacks (
before_save,after_create) only for concerns tightly coupled to persistence. Side effects (emails, jobs) belong in service objects, not callbacks.
- Declare
- Strong parameters: permit only what is needed in
params.require(:model).permit(:field1, :field2). Neverparams.permit!. Extract a privatemodel_paramsmethod in the controller. - Authenticate and authorise:
- Authentication:
has_secure_password+authenticatefor simple apps; Devise for full auth with email confirmation, 2FA, etc. - Authorisation: Pundit (policy objects, clean
authorize @model) or CanCanCan (ability-based). Never inlineif current_user.admin?in views. - API tokens: Devise Token Auth or JWT via
jwtgem with a server-side allowlist for revocation.
- Authentication:
- Background jobs with Sidekiq + ActiveJob:
- Inherit from
ApplicationJob < ActiveJob::Base; setqueue_as :defaultor:critical. - Jobs must be idempotent — they may run more than once (retry on failure).
- Pass only primitive arguments (IDs, strings) — not ActiveRecord objects (they serialise and may be stale).
- Set
sidekiq_options retry: 5, backtrace: 10on the job class for tuning. - Use
Sidekiq::Testing.fake!in tests; assert withassert_enqueued_with.
- Inherit from
- Hotwire (Turbo + Stimulus):
- Turbo Frames (
<turbo-frame id="…">) for lazy-loading and partial page updates. - Turbo Streams for broadcasting real-time updates via
ActionCableor from controller responses. - Stimulus controllers for JavaScript behaviour scoped to a DOM element — never global jQuery-style selectors.
- Avoid full-page redirects where a Turbo Stream response keeps the user in context.
- Turbo Frames (
- Security defaults:
protect_from_forgeryis on by default for HTML responses — never disable it.- Content Security Policy: configure in
config/initializers/content_security_policy.rb— tightenscript-src,style-src,connect-src. config.hosts(Rails 6.1+): restrict to known hostnames in production;allow_all_hostsis development-only.- Force SSL:
config.force_ssl = trueinproduction.rb. - Sensitive attributes:
has_secure_passwordmanages the digest; never store plain passwords.
- Write tests:
- RSpec: model specs (validations, scopes), request specs (API endpoints), system specs (Capybara for full browser flow).
- FactoryBot for test data — define factories, not fixtures, for flexibility.
database_cleaneroruse_transactional_fixtures: trueto reset state between tests.- VCR (
vcrgem) to record and replay external HTTP interactions in tests.
- Audit against
.claude/checklists/security.mdand.claude/checklists/production.md.
Standards
ActiveRecord
belongs_tois required by default (Rails 5+) — addoptional: trueonly when the FK is genuinely nullable.- Add
counter_cache: trueforhas_manycounts that are queried frequently. - Avoid
where("column = '#{value}'")— always use hash syntax or parameterised strings:where(column: value)orwhere("column = ?", value). - Use
find_eachorin_batchesfor bulk processing — never.allon a potentially large table. - Transactions:
ActiveRecord::Base.transaction { ... }for multi-model writes.
Migrations
- Migrations are immutable once run on shared environments — create new migrations, never edit committed ones.
- Use
add_indexwithalgorithm: :concurrentlyon PostgreSQL for zero-lock index creation; note this requires running the migration outside a transaction (disable_ddl_transaction!). - Column removals: two-step — first deploy ignoring the column, then drop it in the next release (avoids
ActiveRecord::UnknownAttributeError). null: falseanddefault:constraints in migrations for required columns.
Services / POROs
- One public method (typically
call), one responsibility. - Accept dependencies via constructor injection — makes testing with mocks straightforward.
- Raise a typed error (
MyService::Error < StandardError) instead of returningfalsefor failure.
Do not
- Do not put business logic in controllers or views.
- Do not use
update_column(skips validations and callbacks) unless you explicitly need that and document why. - Do not use
Model.allwithout alimitin application code paths that could return unbounded rows. - Do not enable
config.allow_all_hostsin staging or production. - Do not use synchronous HTTP calls in a controller action without a timeout; move to a background job.
Common mistakes to avoid
| Mistake | Fix |
|---|---|
N+1 queries from missing includes |
Use includes(:association) or eager_load; detect with bullet gem |
| Callback side-effects (emails, jobs) in ActiveRecord | Move to service objects; trigger from controller after successful save |
params.permit! allowing arbitrary mass assignment |
Explicitly whitelist each attribute in permit(...) |
Memory bloat from Post.all on large tables |
Use find_each(batch_size: 500) or lazy enumerators |
| Synchronous Sidekiq tests asserting on real queue | Use Sidekiq::Testing.fake! in spec helper; Sidekiq::Testing.drain_all to process inline |
| Missing database-level uniqueness constraint | add_index :users, :email, unique: true plus validates :email, uniqueness: true |
String interpolation in where clauses |
Always use where("name = ?", value) or hash syntax to prevent SQL injection |
Output format
- New resource: migration, model with validations and associations, controller with strong params, RSpec request spec.
- Service object: PORO with
callmethod, constructor dependencies, typed error class, and RSpec unit spec. - Background job: ActiveJob class, Sidekiq options, idempotency check, and
assert_enqueued_withtest. - Hotwire interaction: controller action returning
turbo_streamformat, partial template, and Stimulus controller.
Output artifacts go to docs/specs/ for design decisions; code files in app/ tree.
Related checklists
- .claude/checklists/security.md
- .claude/checklists/performance.md
- .claude/checklists/qa.md
- .claude/checklists/production.md
Related agents
- .claude/agents/core/solution-architect.md
- .claude/agents/engineering/backend-engineer.md
- .claude/agents/engineering/database-architect.md
- .claude/agents/quality/security-auditor.md