Ruby on Rails
Ruby on Rails rules and best practices for Claude Code.
7.3k
- Ruby on Rails
- Ruby
- Postgres
- Sidekiq
- Rspec
- Tailwind
5 TEMPLATES
Modern Rails Rules
# CLAUDE.md — Modern Rails ## Stack - Rails 7+ (Rails 8 if you can adopt it). Ruby 3.3+. - The Omakase stack: **Hotwire** (Turbo + Stimulus), **Tailwind**, **importmap-rails** or **esbuild** for JS, **Solid Queue / Solid Cable / Solid Cache** for jobs/cache (Rails 8+). - Skip the SPA. Server-rendered + Turbo handles 90% of "I need it to feel snappy" without React. ## Conventions - "Convention over configuration" is the rule. When in doubt, do what `rails generate` does. - One model per concept. Don't pile unrelated columns onto `User` because every other model joins to it. - Skinny controllers, fat models — but don't let "fat models" become 1000-line god classes. Extract to **POROs** or service objects. ## Generators - Use `bin/rails g model`, `g controller`, `g migration`. The defaults are good. - Don't bypass generators to "save a step" — they keep test files, fixtures, and routes in sync. - Customize generators in `config/generators` once if your defaults differ. ## Strong params - Always permit explicitly: ```ruby params.require(:user).permit(:email, :name, :role) ``` - Never `permit!`. That's an open door. - Define `*_params` private methods on every controller. One per resource action. ## Routes - RESTful first. Add custom actions only when REST genuinely doesn't fit: ```ruby resources :invoices do member { post :send_email } end ``` - Nest at most one level deep. Beyond that, pull a separate controller. ## Concerns - Use `app/models/concerns/` and `app/controllers/concerns/` for cross-cutting behavior with a clear name (`Sluggable`, `SoftDeletable`). - Don't use concerns to "split a long file". Concerns are for reuse, not file organization. ## Service objects - For multi-step operations, write a PORO with a single public method: ```ruby class Invoices::Send def initialize(invoice); @invoice = invoice; end def call; ...; end end ``` - Return a `Result.success(value)` / `Result.failure(error)` object. Don't raise from a service for known failure modes. ## Performance - `bullet` gem in dev. Catches N+1s before they hit prod. - `includes(:assoc)` for eager loading. `.preload` and `.eager_load` if you need to control the join strategy. - `find_each` for iterating large tables. Default `each` loads everything. ## Don't - Don't use callbacks for cross-resource side effects (`after_save` that touches a different table). Use service objects or jobs. - Don't put business logic in views. Helpers exist; presenters / decorators exist. - Don't store sensitive data in `Rails.cache` without `expires_in`. Cache stampedes are real. - Don't disable Rails defaults (CSRF, ForceSSL, Strong Params) without understanding what you give up.Rails ActiveRecord Patterns
# CLAUDE.md — Rails ActiveRecord Patterns ## Migrations - One migration per logical change. Never edit a migration after it's merged — write a new one. - Reversible migrations by default. `change` is fine for simple operations; use `up`/`down` for complex ones. - Add indexes when adding foreign keys: `t.references :user, foreign_key: true, index: true`. - For large data backfills, use a separate `data:migrate` rake task — not the schema migration. ## Models - Skinny associations, smart scopes: ```ruby class Invoice < ApplicationRecord belongs_to :customer has_many :line_items, dependent: :destroy scope :unpaid, -> { where(paid_at: nil) } scope :recent, ->(n = 30) { where("created_at > ?", n.days.ago) } end ``` - Always set `dependent:` on `has_many`. The default is silent orphans. - Validations belong on the model. Don't validate in controllers or service objects. ## Callbacks - Use sparingly. `before_validation` for normalization (downcase email). `after_create_commit` for events that should fire only after the DB is durable. - Don't put cross-model side effects in callbacks. They run in spooky places (specs, console, batch imports). Use service objects. - Skip callbacks intentionally with `update_columns` for one-off backfills — and document why. ## Queries - `find` for primary key (raises if missing). `find_by` for other unique columns (returns nil). - Avoid `find_by_<col>!` — `find_by(col: ...)!` is the modern equivalent. - Eager-load with `includes(:assoc)`. Profile with `bullet` to catch N+1s. - For complex reads, write a query object PORO. Don't bury 30-line scopes in the model. ## Concerns - `app/models/concerns/` for reusable behavior. Name with the capability: `Sluggable`, `SoftDeletable`, `Auditable`. - Concerns hold validations, scopes, and methods that always go together. They don't replace good model design. ## Counter caches & denormalization - Counter caches: `belongs_to :post, counter_cache: true`. Adds `posts_count` increments — saves COUNT queries. - Don't denormalize until you measure the cost. Caches drift. ## Soft delete - If you must, add a `discarded_at:datetime` and use the `discard` gem. - Default scope to non-discarded with care — it's surprising in batch operations. - Most data shouldn't be soft-deleted. The "we might restore it later" is rarely real. ## Don't - Don't use `update_attribute` (singular). It skips validations. - Don't use `unscoped` to "see all rows" in production code. If a default scope hides too much, the scope is wrong. - Don't write SQL via interpolation: `where("name = '#{params[:name]}'")` is injection. Use placeholders. - Don't read all rows then filter in Ruby. Push the filter to SQL.Rails + Hotwire (Stimulus + Turbo)
# CLAUDE.md — Rails + Hotwire (Stimulus + Turbo) ## Mental model - Server renders HTML. **Turbo** intercepts navigation and form submits, swaps fragments without a full reload. **Stimulus** sprinkles behavior onto rendered HTML. - Don't reach for React/Vue. The whole point of Hotwire is staying in Rails. - Default to Turbo Drive for navigation, Turbo Frames for partial updates, Turbo Streams for live updates from the server. ## Turbo Frames - Wrap a region in `<turbo-frame id="...">`. Links and forms inside the frame replace the frame's content with the matching frame in the response. - Frame IDs are stable strings, ideally tied to a model: `dom_id(@invoice)`, `"new_user"`. - Don't nest frames unless you really need scoped navigation. They get confusing fast. ## Turbo Streams - Stream updates from the server push to multiple clients via Action Cable: ```ruby # in the model after_create_commit -> { broadcast_append_to "invoices", target: "invoices_list" } ``` - Stream actions: `append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`. Pick the smallest one that does the job. - Wire up the channel in the view: `<%= turbo_stream_from "invoices" %>`. ## Turbo Drive caveats - Turbo caches pages aggressively. Flag pages that shouldn't cache: `<meta name="turbo-cache-control" content="no-cache">`. - For full reloads (auth flows, marketing pages), opt out: `<a data-turbo="false">`. - `turbo:load` event runs on every navigation. `turbo:before-cache` to clean up before a snapshot. ## Stimulus - One controller per behavior. Files in `app/javascript/controllers/`. - Controller classes match HTML data attributes: ```html <div data-controller="modal" data-modal-open-value="false"> <button data-action="click->modal#toggle">Open</button> <div data-modal-target="content">...</div> </div> ``` - Use **values**, **targets**, **classes**, **outlets** — not arbitrary `dataset` reads. - Don't write a controller for a single use. Inline JS or a Turbo response is often enough. ## Forms - `form_with` uses Turbo by default. Server returns either a redirect (success), a re-rendered form (validation), or a Turbo Stream (in-place update). - For multi-step interactions, use Turbo Frames with frame-specific responses — keeps the URL stable while the user works inside. ## Action Cable - Streams piggyback on Action Cable. Configure Redis (or Solid Cable on Rails 8+) and you're done. - Authenticate channels via `connection.identified_by :current_user` in `ApplicationCable::Connection`. - Don't broadcast PII to public channels. Each user gets their own stream key when needed. ## Don't - Don't ship a Turbo response that contains `<head>` tags. They'll be ignored, and you'll be confused why. - Don't build a Turbo Stream by string-concatenating HTML. Use the `turbo_stream` helpers — they escape correctly. - Don't use `data-turbo-method="delete"` on an `<a>` for destructive actions. Use `button_to` so it's a real form. - Don't put business logic in Stimulus. Stimulus is for UI behavior; data and decisions live on the server.Rails Testing with RSpec
# CLAUDE.md — Rails Testing with RSpec ## Setup - **rspec-rails** + **factory_bot_rails** + **faker** + **shoulda-matchers** + **capybara** (for system specs). - Run with `bin/rspec`. Parallel via `parallel_tests` if your suite is slow enough to matter. - One spec per file under test: `spec/models/user_spec.rb` mirrors `app/models/user.rb`. ## Factories over fixtures - Factories live in `spec/factories/`. One file per model. - Use `Faker` for realistic strings — but seeds the same way every run for stability (`Faker::Config.random = Random.new(42)` in `rails_helper`). - Traits for variations: `factory :user do; trait(:admin) { role :admin } end`. ## Spec types - **Model specs** — validations, scopes, methods. Fast. - **Request specs** — full HTTP round-trip. Replaces controller specs. - **System specs** — Capybara + headless Chrome for end-to-end flows. - Skip controller specs and view specs in new projects. Request and system specs cover their ground. ## Database - `transactional_fixtures = true` in most cases — each example runs in a transaction that rolls back. - For multi-thread (system specs with JS), use **DatabaseCleaner** with `:truncation` strategy. - Don't share state across examples via class-level mutable variables. ## Mocks & stubs - `instance_double(SomeClass, method: value)` — verifies the doubled class actually has the method. - `allow(...).to receive(...)` for stubs (no expectation). `expect(...).to have_received(...)` for verification *after* the action. - Don't mock the framework. If you mock `User.find`, you're testing at the wrong level. ## System specs - Use Capybara's `expect(page).to have_content(...)` and `have_selector(...)` — they retry, accommodating async work. - Avoid `sleep`. If retry-based matchers don't work, the page isn't ready and the test will be flaky. - Run headless in CI; with a visible browser only when debugging locally. ## Speed - Tag slow specs (`:slow`) and exclude from default runs. - `before(:all)` is risky — state leaks across examples. Prefer `before(:each)`. - Use `--profile 10` to find your slowest examples. ## Conventions - One assertion per example where possible. Two when they're tightly related. - `describe` for classes/methods, `context` for situational variants ("when admin", "when guest"). - Error messages are read from the matcher — don't add your own unless it adds info. ## Don't - Don't load Rails for pure-Ruby unit tests. `spec_helper.rb` for those, `rails_helper.rb` for Rails-dependent. - Don't hit external APIs in tests. Stub with **WebMock** + **VCR** (or just WebMock for predictable scenarios). - Don't test private methods directly. They're tested through the public surface. - Don't disable specs. Either fix or delete.Rails Deployment (Kamal + Docker)
# CLAUDE.md — Rails Deployment (Kamal + Docker) ## Why Kamal - Kamal (formerly MRSK) deploys Dockerized apps to bare-metal or VMs without the Kubernetes complexity. - Built into Rails 8. Earlier versions: add the `kamal` gem. - One YAML file (`config/deploy.yml`) describes servers, services, env, and routing. ## Dockerfile - Two-stage build. The Rails 8 default Dockerfile is the right starting point — don't replace it without a reason. - Pin Ruby version: `FROM ruby:3.3-slim`. - Run as a non-root user. - Precompile assets and bootsnap during build, not at boot. - `bundle config set --local without 'development test'` so dev gems don't ship. ## kamal config ```yaml service: myapp image: myorg/myapp servers: web: - 1.2.3.4 job: cmd: bin/jobs hosts: - 1.2.3.4 registry: username: <user> password: - KAMAL_REGISTRY_PASSWORD env: clear: RAILS_ENV: production secret: - DATABASE_URL - SECRET_KEY_BASE ``` - One web role for HTTP, one job role for background workers. Don't run both in one container. - Use Kamal's secrets feature for sensitive env. They never hit disk on the server. ## Migrations - `kamal app exec --reuse 'bin/rails db:migrate'` after deploy, before promoting traffic. - For zero-downtime: backwards-compatible schema changes (add nullable column → deploy → backfill → drop default in next deploy). - Don't run migrations from the web container at boot — race condition with multi-host deploys. ## Background jobs - **Solid Queue** in Rails 8 — runs as a separate process, no Redis needed. - For Sidekiq users: keep the worker container in `servers.workers` and a Redis service. - `bin/jobs` runs Solid Queue. Set `SOLID_QUEUE_IN_PUMA=false` in the worker container. ## Logging & observability - Logs go to STDOUT. Kamal forwards them; aggregate in Loki / Datadog / your SaaS of choice. - `RAILS_LOG_TO_STDOUT=enabled` for the Rails 8 default. JSON formatting via `lograge`. - Healthchecks: Kamal pings `/up`. Make sure that endpoint doesn't query the DB unless you want DB outages to take down the app. ## Database - Managed Postgres (RDS, Crunchy Bridge, Render, Neon). DIY only if you have an SRE on the team. - Connection pooling: `RAILS_MAX_THREADS` × number of pumas × hosts must be ≤ DB max connections. Front with PgBouncer in transaction mode for serverless-style scale. ## Don't - Don't use Capistrano for new Rails apps — Kamal is the modern path. - Don't bake secrets into the Docker image. Use Kamal env or pull from a secret manager. - Don't deploy without a `kamal app version` rollback plan. `kamal rollback` exists; use it. - Don't run `RAILS_ENV=production bin/rails console` against the live DB without ceremony. It's not a sandbox.
Have a CLAUDE.md template that works for you?
Send it in — we’ll credit you and publish it under the right tags.