home / skills / majesticlabs-dev / majestic-marketplace / dhh-coder

dhh-coder skill

/plugins/majestic-rails/skills/dhh-coder

npx playbooks add skill majesticlabs-dev/majestic-marketplace --skill dhh-coder

Review the files below or copy the command above to add this skill to your agents.

Files (22)
SKILL.md
11.4 KB
---
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

```ruby
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:

```ruby
  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:

```ruby
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:

```ruby
class Current < ActiveSupport::CurrentAttributes
  attribute :user, :session
end

# Usage anywhere in app
Current.user.can_administer?(@message)
```

### Ruby Syntax Preferences

```ruby
# 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

```ruby
# 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:

```ruby
class Event < ApplicationRecord
  def action
    super.inquiry
  end
end

# Clean predicate methods
event.action.completed?
event.action.pending?
event.action.failed?
```

### Controller Response Patterns

```ruby
# 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`:

```ruby
# 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:

```ruby
# 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:

```ruby
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:

```ruby
# In model
def broadcast_create
  broadcast_append_to room, :messages, target: "messages"
end
```

**For detailed Hotwire patterns, use `hotwire-coder` skill.**

### Error Handling
Rescue specific exceptions, fail fast with bang methods:

```ruby
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:

```ruby
# 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` |

```ruby
# 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 explanations
- `references/palkan-patterns.md` - Namespaced model classes, counter caches, model organization order, PostgreSQL enums
- `references/concerns-organization.md` - Model-specific vs common concerns, facade pattern
- `references/delegated-types.md` - Polymorphism without STI problems
- `references/recording-pattern.md` - Unifying abstraction for diverse content types
- `references/filter-objects.md` - PORO filter objects, URL-based state, testable query building
- `references/database-patterns.md` - UUIDv7, hard deletes, state as records, counter caches, indexing

### Rails Components
- `references/activerecord-tips.md` - ActiveRecord query patterns, validations, associations
- `references/controllers-tips.md` - Controller patterns, routing, rate limiting, form objects
- `references/activestorage-tips.md` - File uploads, attachments, blob handling

### Hotwire
- `references/hotwire-tips.md` - Turbo Frames, Turbo Streams, ViewComponents
- `references/turbo-morphing.md` - Turbo 8 page refresh with morphing patterns
- `references/stimulus-catalog.md` - Copy-paste-ready Stimulus controllers (clipboard, dialog, hotkey, etc.)
- **Also see:** `hotwire-coder`, `stimulus-coder`, `viewcomponent-coder` skills for detailed patterns

### 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 model
- `references/multi-tenancy.md` - Path-based tenancy, cookie scoping, tenant-aware jobs

### Infrastructure & Integrations
- `references/webhooks.md` - Secure webhook delivery, SSRF protection, retry strategies
- `references/caching-strategies.md` - Russian Doll caching, Solid Cache, cache analysis
- `references/config-tips.md` - Configuration, logging, deployment patterns
- `references/structured-events.md` - Rails 8.1 `Rails.event` API for structured observability
- `references/resources.md` - Links to source material and further reading

## Philosophy Summary

1. **REST purity**: 7 actions only; new controllers for variations
2. **Fat models**: Authorization, broadcasting, business logic in models
3. **Thin controllers**: 1-5 line actions; extract complexity
4. **Convention over configuration**: Empty methods, implicit rendering
5. **Minimal abstractions**: No service objects for simple cases
6. **Current attributes**: Thread-local request context everywhere
7. **Hotwire-first**: Model-level broadcasting, Turbo Streams, Stimulus
8. **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 `pluck` over `map` for attribute extraction
- [ ] Current user resources use `My::` namespace
- [ ] Data computed at write time, not presentation