// Write Ruby and Rails code in DHH's distinctive 37signals style. Use this skill when writing Ruby code, Rails applications, creating models, controllers, or any Ruby file. Triggers on Ruby/Rails code generation, refactoring requests, or when the user mentions DHH, 37signals, Basecamp, HEY, Fizzy, or Campfire style.
| name | dhh-coder |
| description | Write Ruby and Rails code in DHH's distinctive 37signals style. Use this skill when writing Ruby code, Rails applications, creating models, controllers, or any Ruby file. Triggers on Ruby/Rails code generation, refactoring requests, or when the user mentions DHH, 37signals, Basecamp, HEY, Fizzy, or Campfire style. |
Write Ruby and Rails code following DHH's philosophy: clarity over cleverness, convention over configuration, developer happiness above all.
Patterns derived from 37signals' production applications: Basecamp, HEY, Campfire, and the open-source Fizzy SaaS project.
index, show, new, create, edit, update, destroyclass MessagesController < ApplicationController
before_action :set_message, only: %i[ show edit update destroy ]
def index
@messages = @room.messages.with_creator.last_page
fresh_when @messages
end
def show
end
def create
@message = @room.messages.create_with_attachment!(message_params)
@message.broadcast_create
end
private
def set_message
@message = @room.messages.find(params[:id])
end
def message_params
params.require(:message).permit(:body, :attachment)
end
end
Indent private methods one level under private keyword:
private
def set_message
@message = Message.find(params[:id])
end
def message_params
params.require(:message).permit(:body)
end
Models own business logic, authorization, and broadcasting:
class Message < ApplicationRecord
belongs_to :room
belongs_to :creator, class_name: "User"
has_many :mentions
scope :with_creator, -> { includes(:creator) }
scope :page_before, ->(cursor) { where("id < ?", cursor.id).order(id: :desc).limit(50) }
def broadcast_create
broadcast_append_to room, :messages, target: "messages"
end
def mentionees
mentions.includes(:user).map(&:user)
end
end
class User < ApplicationRecord
def can_administer?(message)
message.creator == self || admin?
end
end
Use Current for request context, never pass current_user everywhere:
class Current < ActiveSupport::CurrentAttributes
attribute :user, :session
end
# Usage anywhere in app
Current.user.can_administer?(@message)
# Symbol arrays with spaces inside brackets
before_action :set_message, only: %i[ show edit update destroy ]
# Modern hash syntax exclusively
params.require(:message).permit(:body, :attachment)
# Single-line blocks with braces
users.each { |user| user.notify }
# Ternaries for simple conditionals
@room.direct? ? @room.users : @message.mentionees
# Bang methods for fail-fast
@message = Message.create!(params)
@message.update!(message_params)
# Predicate methods with question marks
@room.direct?
user.can_administer?(@message)
@messages.any?
# Expression-less case for cleaner conditionals
case
when params[:before].present?
@room.messages.page_before(params[:before])
when params[:after].present?
@room.messages.page_after(params[:after])
else
@room.messages.last_page
end
| Element | Convention | Example |
|---|---|---|
| Setter methods | set_ prefix | set_message, set_room |
| Parameter methods | {model}_params | message_params |
| Association names | Semantic, not generic | creator not user |
| Scopes | Chainable, descriptive | with_creator, page_before |
| Predicates | End with ? | direct?, can_administer? |
Broadcasting is model responsibility:
# In model
def broadcast_create
broadcast_append_to room, :messages, target: "messages"
end
# In controller
@message.broadcast_replace_to @room, :messages,
target: [ @message, :presentation ],
partial: "messages/presentation",
attributes: { maintain_scroll: true }
Rescue specific exceptions, fail fast with bang methods:
def create
@message = @room.messages.create_with_attachment!(message_params)
@message.broadcast_create
rescue ActiveRecord::RecordNotFound
render action: :room_not_found
end
| Traditional | DHH Way |
|---|---|
| PostgreSQL | SQLite (for single-tenant) |
| Redis + Sidekiq | Solid Queue |
| Redis cache | Solid Cache |
| Kubernetes | Single Docker container |
| Service objects | Fat models |
| Policy objects (Pundit) | Authorization on User model |
| FactoryBot | Fixtures |
For comprehensive patterns and examples, see:
references/patterns.md - Complete code patterns with explanationsreferences/palkan-patterns.md - Namespaced model classes, counter caches, model organization order, PostgreSQL enumsreferences/concerns-organization.md - Model-specific vs common concerns, facade patternreferences/delegated-types.md - Polymorphism without STI problemsreferences/recording-pattern.md - Unifying abstraction for diverse content typesreferences/filter-objects.md - PORO filter objects, URL-based state, testable query buildingreferences/activerecord-tips.md - ActiveRecord query patterns, validations, associationsreferences/controllers-tips.md - Controller patterns, routing, rate limiting, form objectsreferences/hotwire-tips.md - Turbo Frames, Turbo Streams, Stimulus, ViewComponentsreferences/turbo-morphing.md - Turbo 8 page refresh with morphing patternsreferences/activestorage-tips.md - File uploads, attachments, blob handlingreferences/stimulus-catalog.md - Copy-paste-ready Stimulus controllers (clipboard, dialog, hotkey, etc.)references/css-architecture.md - Native CSS patterns (layers, OKLCH, nesting, dark mode)references/passwordless-auth.md - Magic link authentication, sessions, identity modelreferences/multi-tenancy.md - Path-based tenancy, cookie scoping, tenant-aware jobsreferences/webhooks.md - Secure webhook delivery, SSRF protection, retry strategiesreferences/caching-strategies.md - Russian Doll caching, Solid Cache, cache analysisreferences/config-tips.md - Configuration, logging, deployment patternsreferences/resources.md - Links to source material and further reading