Rule Writing

CLAUDE.md for Ruby on Rails

Rails has a convention for everything — AI ignores them all. Rules for the Rails Way: ActiveRecord, concerns, turbo/stimulus, strong params, and the asset pipeline.

8 min read·April 3, 2025

Rails conventions are load-bearing architecture — AI ignores them all

Skinny controllers, ActiveRecord, Hotwire, strong params, and the Rails Way

Why Rails Needs Rules That Enforce the Rails Way

Rails is the most convention-driven framework in existence — convention over configuration is its founding principle. Every file has a place, every class has a naming pattern, and every operation has a Rails Way to do it. AI assistants ignore all of this, generating code that fights Rails: fat controllers with 30 actions, bypassed ActiveRecord with raw SQL, manual authentication instead of Devise or has_secure_password, and React SPAs instead of Hotwire.

The damage from ignoring Rails conventions isn't just style — it's functional. Rails generators, migrations, autoloading, and the asset pipeline all depend on correct file naming and placement. A model in the wrong directory isn't autoloaded. A controller with the wrong name doesn't match the route. Convention isn't optional in Rails — it's load-bearing architecture.

These rules target Rails 7.1+ with Hotwire (Turbo + Stimulus). If you're on an older version or using a different frontend approach, adjust the Hotwire-specific rules.

Rule 1: Skinny Controllers, Fat Models (Sort Of)

The rule: 'Controllers have one responsibility: receive the request, call the model/service, respond. Each action is under 10 lines. No business logic in controllers — delegate to models, service objects, or form objects. Use before_action for shared logic (authentication, loading records). Use strong parameters for input whitelisting: params.require(:user).permit(:name, :email).'

For the model layer: 'Models contain: associations, validations, scopes, and simple business methods. When a model exceeds 200 lines or has logic that doesn't fit the model's core responsibility, extract to: service objects (app/services/) for complex operations, form objects (app/forms/) for multi-model forms, query objects (app/queries/) for complex queries, and presenters (app/presenters/) for view logic.'

AI generates 50-line controller actions with database queries, conditionals, email sending, and response formatting inline. The skinny controller rule forces delegation — the controller is a traffic cop, not a business logic container.

  • Controller actions under 10 lines — delegate to models/services
  • before_action for shared logic — auth, record loading, setup
  • Strong params: params.require(:model).permit(:field1, :field2)
  • Models: associations, validations, scopes, simple methods — under 200 lines
  • Extract to services/forms/queries when model gets complex
💡 10 Lines Per Action

Controller actions under 10 lines. Receive request → call service → respond. No conditionals, no queries, no email sending inline. The controller is a traffic cop, not a business logic container.

Rule 2: ActiveRecord the Rails Way

The rule: 'Use ActiveRecord for all database operations — never raw SQL unless ActiveRecord genuinely can't express it (rare). Use scopes for reusable query conditions: scope :active, -> { where(active: true) }. Use associations: has_many, belongs_to, has_one, has_many :through. Use callbacks sparingly — only for data integrity (setting defaults, normalizing data). Never use callbacks for business logic (sending emails, creating related records).'

For validations: 'Validate at the model level: validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }. Use custom validators for complex rules. Use database constraints (NOT NULL, UNIQUE indexes) as a safety net alongside model validations. Validations are the first line of defense, constraints are the last.'

For migrations: 'Use rails generate migration for all schema changes. One concept per migration. Use reversible migrations — always include both up and down. Use change method when Rails can infer the reverse. Add indexes for foreign keys and frequently queried columns. Never modify data in schema migrations — use separate data migrations.'

⚠️ Callbacks ≠ Business Logic

ActiveRecord callbacks (after_create, before_save) are for data integrity: normalizing, setting defaults. Never use them for business logic: sending emails, creating related records. That's what service objects are for.

Rule 3: Hotwire (Turbo + Stimulus)

The rule: 'Use Hotwire for interactivity — not React, Vue, or jQuery. Turbo Drive for page navigation (automatic, no setup). Turbo Frames for updating page sections without full reload: <turbo-frame id="user_form">. Turbo Streams for real-time updates from the server: turbo_stream.replace, turbo_stream.append. Stimulus for JavaScript behavior on HTML elements: data-controller, data-action.'

For Turbo Frames: 'Wrap the section you want to update independently: <turbo-frame id="comments">. Links and forms inside a frame update only that frame. Use data-turbo-frame="_top" to break out of a frame. Use lazy loading: <turbo-frame id="sidebar" src="/sidebar" loading="lazy">.'

For Stimulus: 'Stimulus controllers add behavior to HTML: <div data-controller="toggle" data-toggle-class="hidden">. Controllers live in app/javascript/controllers/. Keep controllers small — one behavior per controller. Use targets for element references: static targets = ["content"]. Use values for configuration: static values = { url: String }.'

  • Turbo Drive: automatic SPA-like navigation — zero JavaScript needed
  • Turbo Frames: update sections independently — <turbo-frame id='...'>
  • Turbo Streams: server-push updates — replace, append, prepend, remove
  • Stimulus: behavior on HTML — data-controller, data-action, targets, values
  • No React/Vue/jQuery — Hotwire handles 95% of interactivity needs
Hotwire Over React

Turbo Drive + Frames + Streams + Stimulus handle 95% of interactivity needs without React, Vue, or a build step. AI reaches for React because it dominates training data. Rails 7+ defaults to Hotwire for a reason.

Rule 4: Testing the Rails Way

The rule: 'Use Minitest (Rails default) or RSpec — pick one and be consistent. Use fixtures or FactoryBot for test data. Use system tests (driven by Capybara + Selenium/Playwright) for full-stack feature testing. Use integration tests for request/response cycles. Use unit tests for models and service objects. Follow the testing pyramid: many unit tests, fewer integration tests, fewest system tests.'

For system tests: 'System tests drive a real browser — they test what the user sees. Use driven_by :selenium, using: :headless_chrome. Test user flows: sign up, create a record, verify it appears. Use assert_selector, fill_in, click_on. System tests are slow — test happy paths and critical error paths, not every edge case.'

For model tests: 'Test validations: assert_not user.valid? when required field is blank. Test scopes: assert_equal expected, User.active. Test associations: assert_respond_to user, :orders. Test business methods with specific inputs and expected outputs. Use fixtures or factories — never create objects inline in every test.'

Rule 5: Essential Rails Conventions

The rule: 'Use Rails generators for scaffolding: rails g model, rails g controller, rails g migration. Follow naming: singular models (User), plural controllers (UsersController), plural tables (users). Use concerns for shared model/controller behavior: include Authenticatable. Use I18n for all user-facing text — never hardcode strings. Use credentials for secrets: rails credentials:edit.'

For the asset pipeline: 'Use importmap (Rails 7 default) or jsbundling-rails for JavaScript. Use cssbundling-rails or Tailwind for CSS. Use ActiveStorage for file uploads — never manual file handling. Use ActionMailer for email — never raw SMTP. Use ActionCable for WebSockets — never raw WebSocket handling.'

For background jobs: 'Use ActiveJob with Sidekiq (or GoodJob, Solid Queue). Queue long-running operations: email sending, file processing, external API calls. Never perform these in the request cycle — the user waits. Use perform_later for async, perform_now only in tests and rare synchronous cases.'

  • Rails generators for all scaffolding — consistent naming and placement
  • Singular models (User), plural controllers (UsersController), plural tables (users)
  • Concerns for shared behavior — I18n for all strings — credentials for secrets
  • importmap or jsbundling — ActiveStorage for files — ActionMailer for email
  • ActiveJob + Sidekiq for background — never long tasks in request cycle

Complete Rails Rules Template

Consolidated rules for Ruby on Rails 7.1+ projects.

  • Skinny controllers (<10 lines/action) — delegate to models, services, forms
  • Strong params: require + permit — before_action for shared logic
  • ActiveRecord: scopes, associations, validations — never raw SQL — callbacks sparingly
  • Hotwire: Turbo Drive + Frames + Streams for interactivity, Stimulus for JS behavior
  • Minitest or RSpec — fixtures or FactoryBot — system tests for user flows
  • Rails generators — singular models, plural controllers — concerns for shared behavior
  • ActiveJob + Sidekiq — never long tasks in request cycle
  • rubocop-rails in CI — brakeman for security — bullet for N+1 detection
CLAUDE.md for Ruby on Rails — RuleSync Blog