Ruby on Rails AI Rules

Rules for Rails 8 projects: convention-over-configuration discipline, Hotwire-first UI, ActiveRecord patterns, fat models / skinny controllers, and system tests.

RubyRails#ruby#rails#hotwire#turboLast updated 2026-05-05
tune

Want to customize this rules file? Open the generator with this stack pre-loaded.

Open in generatorarrow_forward

Save at .cursor/rules/main.mdc

Ruby on Rails

Project context

This is a Rails 8 project. We follow Rails conventions ruthlessly — fighting them costs more than it's worth. Hotwire-first UI (Turbo + Stimulus); avoid SPA frameworks unless the product genuinely demands one.

Stack

  • Ruby 3.3+
  • Rails 8+
  • Postgres 15+
  • Hotwire (Turbo, Stimulus)
  • Tailwind CSS via tailwindcss-rails
  • Sidekiq for background jobs (or Solid Queue, the new Rails default)
  • RSpec or Minitest (consistent across the project)
  • Standard or RuboCop for lint
  • Bun or import maps for JS

Folder structure (Rails defaults)

app/
  controllers/
  models/
  views/
  helpers/
  jobs/
  mailers/
  components/        — view components (if using ViewComponent gem)
  javascript/
    controllers/     — Stimulus controllers
config/
db/
  migrate/
  schema.rb
test/  or  spec/

Don't reinvent. If your code doesn't fit Rails defaults, it's usually wrong.

Models

  • One model per database table
  • Validations on the model — not just at the form level
  • Scopes for reusable queries: scope :published, -> { where.not(published_at: nil) }
  • Associations declared explicitly: has_many :posts, dependent: :destroy
  • Use enum for state-like fields with a fixed set of values
  • Use ActiveRecord::Encryption for sensitive fields
class Post < ApplicationRecord
  belongs_to :author, class_name: "User"
  has_many :comments, dependent: :destroy

  validates :title, presence: true, length: { maximum: 200 }

  scope :published, -> { where.not(published_at: nil) }
  scope :recent,    ->(n = 20) { order(created_at: :desc).limit(n) }

  enum :status, { draft: 0, published: 1, archived: 2 }
end

Controllers

  • Skinny — call models / services, render
  • One CRUD action per public method (index, show, new, create, edit, update, destroy)
  • Use strong parameters: params.expect(post: [:title, :body]) (Rails 8) or params.require(:post).permit(:title, :body)
  • Handle respond_to blocks for HTML and Turbo Stream responses
class PostsController < ApplicationController
  def create
    @post = Current.user.posts.build(post_params)
    if @post.save
      respond_to do |f|
        f.html { redirect_to @post }
        f.turbo_stream
      end
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def post_params = params.expect(post: [:title, :body])
end

Hotwire / Turbo / Stimulus

  • Use Turbo Frames to scope partial updates (<turbo-frame id="...">)
  • Use Turbo Streams to broadcast updates over WebSocket (Turbo::StreamsChannel.broadcast_*)
  • Use Stimulus for behavior; don't hand-roll vanilla JS
  • One Stimulus controller per behavior; small, focused
  • Keep state in the DOM where possible

Migrations

  • bin/rails generate migration AddTitleToPosts title:string
  • Always set null: false and a default for new columns on existing tables
  • Add indexes (add_index) for any column used in where filters
  • Reversible migrations: provide change, or both up and down
  • Never edit a committed migration; create a new one

Background jobs

  • ActiveJob with Solid Queue (Rails 8 default) or Sidekiq
  • Idempotent jobs — they will be retried; design for it
  • Don't pass complex objects into jobs; pass IDs and re-fetch
class NotifyJob < ApplicationJob
  queue_as :default
  retry_on ApiError, wait: :polynomially_longer, attempts: 5

  def perform(user_id, post_id)
    user = User.find(user_id)
    post = Post.find(post_id)
    # ...
  end
end

Service objects (when controllers grow)

  • Plain Ruby classes in app/services/
  • One public method (call); maybe a class-level .call(...)
  • Return rich result objects, not just booleans, when callers need details

Testing

  • RSpec or Minitest — pick one and stick with it
  • Model specs for validations, scopes, business logic
  • Request specs for controllers (over view specs)
  • System specs (Capybara) for critical user flows
  • Use FactoryBot for fixtures
  • rails test:system runs system tests; slow but real

Patterns to follow

  • Use Current.user (ActiveSupport::CurrentAttributes) instead of threading current_user through everywhere
  • Use dependent: :destroy deliberately — sometimes you want :nullify or nothing
  • Use with_lock for SELECT FOR UPDATE; transaction for multi-step writes
  • Prefer database-level constraints (null: false, foreign keys) over app-only validations

Patterns to avoid

  • Skinny models, fat controllers — Rails wants the inverse
  • before_filter / before_action for too much logic — extract a service
  • Editing a committed migration — create a new one
  • Skipping callbacks (save(validate: false), update_columns) without good reason
  • Long-running work in controllers — push to a job
  • N+1 queries — use includes / preload / eager_load; the bullet gem will flag them in dev

Tooling

  • bin/dev — Procfile-based dev (Rails + Tailwind watcher + JS watcher)
  • bin/rails console — REPL
  • bin/rails db:migrate
  • bundle exec rspec (or rails test)
  • bundle exec standardrb (or rubocop)

AI behavioral rules

  • Always run bin/rails generate migration for schema changes — never hand-edit schema.rb
  • Validations belong on the model, not the form
  • Use Hotwire defaults (Turbo Frame / Stream) for UI updates before suggesting JS or React
  • Never modify a committed migration; always create a new one
  • Don't update_columns / save(validate: false) without justifying why
  • Default to RSpec system tests for user flows; controller tests for API endpoints
  • Run the full test suite and the linter before declaring a task done

Frequently asked

How do I use this Rails rules file with Cursor?

Pick "Cursor (.cursor/rules/*.mdc)" from the format dropdown above and click Copy. Save it at .cursor/rules/main.mdc in your project root and restart Cursor. The legacy .cursorrules format still works if you're on an older Cursor version — pick that option instead.

Can I use this with Claude Code (CLAUDE.md)?

Yes — pick "Claude Code (CLAUDE.md)" from the format dropdown above and copy. Save the file as CLAUDE.md at your repo root. Claude Code reads it automatically on every session. For monorepos, you can also drop nested CLAUDE.md files in subdirectories — Claude merges them when working in those paths.

Where exactly do I put this file?

It depends on the AI tool. Cursor reads .cursorrules or .cursor/rules/*.mdc at the project root. Claude reads CLAUDE.md at the project root. Copilot reads .github/copilot-instructions.md. The "Save at" path under each format in the dropdown shows the exact location for the format you picked.

Can I customize these Rails rules for my project?

Yes — that's what the generator is for. Click "Open in generator" above and the wizard loads with this stack's defaults pre-selected. Toggle on or off the conventions you want, then re-export in your AI tool's format.

Will using this rules file slow down my AI tool?

No. Rules files count toward the model's context window but not toward latency in any noticeable way. The file is loaded once per session, not per token. The library files target 250–400 lines, well within every tool's recommended budget.

Should I commit this file to git?

Yes. The rules file is project documentation that benefits every developer using the AI tool. Commit it. The exception is personal-global settings (e.g. ~/.claude/CLAUDE.md) which are user-scoped and stay out of the repo.