AgentSkillsCN

rails-best-practices

遵循37signals/DHH模式构建Rails应用的指南。适用于开发Rails应用时:(1) 控制器设计与路由;(2) 模型关注点与业务逻辑;(3) 不使用Devise的认证方案;(4) Hotwire/Turbo/Stimulus模式;(5) CSS架构;(6) 使用Minitest进行测试。强调原生Rails、丰富的模型、精简的控制器、全方位的CRUD操作,以及以数据库为后盾、而非Redis的基础设施。

SKILL.md
--- frontmatter
name: rails-best-practices
description: "Guide for building Rails applications following 37signals/DHH patterns. Use when developing Rails apps for: (1) Controller design and routing, (2) Model concerns and business logic, (3) Authentication without Devise, (4) Hotwire/Turbo/Stimulus patterns, (5) CSS architecture, (6) Testing with Minitest. Emphasizes vanilla Rails, rich models, thin controllers, CRUD-everything, and database-backed infrastructure over Redis."

Rails Best Practices (37signals Style)

Patterns extracted from 37signals' Fizzy codebase - production Rails code demonstrating "vanilla Rails is plenty."

Core Philosophy

  1. Rich domain models over service objects
  2. CRUD controllers over custom actions
  3. Concerns for horizontal code sharing
  4. Records as state over boolean columns
  5. Database-backed everything (no Redis)
  6. 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

PatternWhy Avoid
deviseAuth is ~150 lines of custom code
pundit/cancancanAuthorization lives in models
dry-rb gemsOver-engineered for most apps
interactor/commandService objects rarely needed
view_componentERB partials are fine
sidekiq + RedisUse Solid Queue (database-backed)
rspecMinitest 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: