Rails Best Practices (37signals Style)
Patterns extracted from 37signals' Fizzy codebase - production Rails code demonstrating "vanilla Rails is plenty."
Core Philosophy
- •Rich domain models over service objects
- •CRUD controllers over custom actions
- •Concerns for horizontal code sharing
- •Records as state over boolean columns
- •Database-backed everything (no Redis)
- •Build it yourself before reaching for gems
Quick Reference
Routing: Everything is CRUD
ruby
# BAD: Custom actions resources :cards do post :close post :reopen end # GOOD: New resources for state changes resources :cards do resource :closure # POST to close, DELETE to reopen resource :pin # POST to pin, DELETE to unpin resource :watch # POST to watch, DELETE to unwatch end
Controller Design
ruby
# Thin controller - model does the work
class Cards::ClosuresController < ApplicationController
include CardScoped
def create
@card.close # All logic in model
respond_to do |format|
format.turbo_stream { render_card_replacement }
format.json { head :no_content }
end
end
def destroy
@card.reopen
respond_to do |format|
format.turbo_stream { render_card_replacement }
format.json { head :no_content }
end
end
end
Model Concerns
ruby
# Self-contained behavior with associations, scopes, methods
module Card::Closeable
extend ActiveSupport::Concern
included do
has_one :closure, dependent: :destroy
scope :closed, -> { joins(:closure) }
scope :open, -> { where.missing(:closure) }
end
def closed? = closure.present?
def open? = !closed?
def close(user: Current.user)
transaction do
create_closure!(user: user)
track_event :closed, creator: user
end unless closed?
end
def reopen(user: Current.user)
transaction do
closure&.destroy
track_event :reopened, creator: user
end if closed?
end
end
State as Records
ruby
# Instead of boolean columns, create records class Closure < ApplicationRecord belongs_to :card, touch: true belongs_to :user, optional: true # created_at = when, user = who end # Query patterns Card.open # where.missing(:closure) Card.closed # joins(:closure)
Default Values via Lambdas
ruby
class Card < ApplicationRecord
belongs_to :account, default: -> { board.account }
belongs_to :creator, class_name: "User", default: -> { Current.user }
end
Current for Request Context
ruby
class Current < ActiveSupport::CurrentAttributes attribute :session, :user, :identity, :account attribute :request_id, :user_agent, :ip_address end
What to Avoid
| Pattern | Why Avoid |
|---|---|
devise | Auth is ~150 lines of custom code |
pundit/cancancan | Authorization lives in models |
dry-rb gems | Over-engineered for most apps |
interactor/command | Service objects rarely needed |
view_component | ERB partials are fine |
sidekiq + Redis | Use Solid Queue (database-backed) |
rspec | Minitest is simpler and faster |
Gems They DO Use
- •
solid_queue- Database-backed jobs - •
solid_cache- Database-backed caching - •
solid_cable- Database-backed WebSockets - •
geared_pagination- Cursor-based pagination - •
propshaft- Asset pipeline - •
kamal- Docker deployment
Detailed References
For comprehensive patterns and examples, see:
- •references/controllers.md - Controller concerns, scoping, authorization
- •references/models.md - Concerns catalog, callbacks, scopes
- •references/authentication.md - Passwordless magic link auth
- •references/hotwire.md - Turbo Streams, Stimulus controllers
- •references/css.md - Modern CSS with layers, OKLCH, nesting
- •references/testing.md - Minitest patterns, fixtures
- •references/stimulus-catalog.md - Reusable Stimulus controllers