dhh-coder
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.
$ Installieren
git clone https://github.com/majesticlabs-dev/majestic-marketplace /tmp/majestic-marketplace && cp -r /tmp/majestic-marketplace/plugins/majestic-rails/skills/dhh-coder ~/.claude/skills/majestic-marketplace// tip: Run this command in your terminal to install the skill
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.
DHH Ruby/Rails Style Guide
Write Ruby and Rails code following DHH's philosophy: clarity over cleverness, convention over configuration, developer happiness above all.
Quick Reference
Controller Actions
- Only 7 REST actions:
index,show,new,create,edit,update,destroy - New behavior? Create a new controller, not a custom action
- Action length: 1-5 lines maximum
- Empty actions are fine: Let Rails convention handle rendering
class 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
Private Method Indentation
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
Model Design (Fat Models)
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
Current Attributes
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)
Ruby Syntax Preferences
# 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
Query Optimization
# WRONG: Load all records then extract attribute
users.map(&:name)
# CORRECT: Pluck directly from database
users.pluck(:name)
# WRONG: Count via Ruby
messages.to_a.count
# CORRECT: Count via SQL
messages.count
StringInquirer for Predicates
Use .inquiry on string enums for readable conditionals:
class Event < ApplicationRecord
def action
super.inquiry
end
end
# Clean predicate methods
event.action.completed?
event.action.pending?
event.action.failed?
Controller Response Patterns
# Return 204 No Content for successful updates without body
def update
@message.update!(message_params)
head :no_content
end
# Return 201 Created for successful creates
def create
@message = Message.create!(message_params)
head :created
end
My:: Namespace for Current User Resources
Use My:: namespace for resources scoped to Current.user:
# routes.rb
namespace :my do
resource :profile, only: %i[ show edit update ]
resources :notifications, only: %i[ index destroy ]
end
# app/controllers/my/profiles_controller.rb
class My::ProfilesController < ApplicationController
def show
@profile = Current.user
end
end
No index or show with ID needed—resource is implicit from Current.user.
Compute at Write Time
Perform data manipulation during saves, not during presentation:
# WRONG: Compute on read
def display_name
"#{first_name} #{last_name}".titleize
end
# CORRECT: Compute on write
before_save :set_display_name
private
def set_display_name
self.display_name = "#{first_name} #{last_name}".titleize
end
Benefits: enables pagination, caching, and reduces view complexity.
Delegate for Lazy Loading
Use delegate to enable lazy loading through associations:
class Message < ApplicationRecord
belongs_to :session
delegate :user, to: :session
end
# Lazy loads user through session
message.user
Naming Conventions
| 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? |
| Current user resources | My:: namespace | My::ProfilesController |
Hotwire/Turbo Patterns
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 }
Error Handling
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
State as Records (Not Booleans)
Track state via database records rather than boolean columns:
# WRONG: Boolean columns for state
class Card < ApplicationRecord
# closed: boolean, gilded: boolean columns
end
card.update!(closed: true)
card.closed? # Loses who/when/why
# CORRECT: State as separate records
class Card < ApplicationRecord
has_one :closure
has_one :gilding
def close(by:)
create_closure!(closed_by: by)
end
def closed?
closure.present?
end
end
card.close(by: Current.user)
card.closure.closed_by # Full audit trail
REST URL Transformations
Map custom actions to nested resource controllers:
| Custom Action | REST Resource |
|---|---|
POST /cards/:id/close | POST /cards/:id/closure |
DELETE /cards/:id/close | DELETE /cards/:id/closure |
POST /cards/:id/gild | POST /cards/:id/gilding |
POST /posts/:id/publish | POST /posts/:id/publication |
DELETE /posts/:id/publish | DELETE /posts/:id/publication |
# routes.rb
resources :cards do
resource :closure, only: %i[ create destroy ]
resource :gilding, only: %i[ create destroy ]
end
# app/controllers/cards/closures_controller.rb
class Cards::ClosuresController < ApplicationController
def create
@card = Card.find(params[:card_id])
@card.close(by: Current.user)
end
def destroy
@card = Card.find(params[:card_id])
@card.closure.destroy!
end
end
Architecture Preferences
| 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 |
| Boolean state columns | State as records |
Detailed References
For comprehensive patterns and examples, see:
Core Patterns
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/database-patterns.md- UUIDv7, hard deletes, state as records, counter caches, indexing
Rails Components
references/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.)
Frontend
references/css-architecture.md- Native CSS patterns (layers, OKLCH, nesting, dark mode)
Authentication & Multi-Tenancy
references/passwordless-auth.md- Magic link authentication, sessions, identity modelreferences/multi-tenancy.md- Path-based tenancy, cookie scoping, tenant-aware jobs
Infrastructure & Integrations
references/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/structured-events.md- Rails 8.1Rails.eventAPI for structured observabilityreferences/resources.md- Links to source material and further reading
Philosophy Summary
- REST purity: 7 actions only; new controllers for variations
- Fat models: Authorization, broadcasting, business logic in models
- Thin controllers: 1-5 line actions; extract complexity
- Convention over configuration: Empty methods, implicit rendering
- Minimal abstractions: No service objects for simple cases
- Current attributes: Thread-local request context everywhere
- Hotwire-first: Model-level broadcasting, Turbo Streams, Stimulus
- Readable code: Semantic naming, small methods, no comments needed
Success Indicators
Code aligns with DHH style when:
- Controllers map CRUD verbs to resources (no custom actions)
- Models use concerns for horizontal behavior sharing
- State uses records instead of boolean columns
- Abstractions remain minimal (no unnecessary service objects)
- Database backs solutions (Solid Queue/Cache, not Redis)
- Turbo/Stimulus handle all interactivity
- Authorization lives on User model (
can_*?methods) - Current attributes provide request context
- Scopes follow naming conventions (
chronologically,with_*, etc.) - Uses
pluckovermapfor attribute extraction - Current user resources use
My::namespace - Data computed at write time, not presentation
Repository
