dhh-coder について
dhh-coderは、DHHの方針に沿った実装や設計をまとめた、Ruby・Rails開発者向けのコーディングスタイルスキルです。RESTコントローラー、ファットモデル、命名、クエリ最適化、Hotwireのパターンを扱います。
主な機能
- 01RESTの7つのコントローラーアクションを使用
- 02ビジネスロジックをモデルに配置
- 03リクエストコンテキストにCurrentを使用
- 04データベースのクエリと書き込みを最適化
- 05RailsとHotwireのパターンを紹介
活用シーン
- 01DHHのスタイルでRails機能を実装
- 02コントローラーとモデルの責務を整理
- 03ActiveRecordクエリを最適化
- 04レコードによる状態管理とRESTルートを設計
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ majesticlabs-dev/ majestic-marketplace --skill dhh-coderインストール後、Claude Code で/dhh-coderを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/activerecord-tips.mdactivestorage-tips.mdcaching-strategies.mdconcerns-organization.mdconfig-tips.mdcontrollers-tips.mdcss-architecture.mddatabase-patterns.mddelegated-types.mdfilter-objects.mdhotwire-tips.mdmulti-tenancy.mdpalkan-patterns.mdpasswordless-auth.mdpatterns.mdrecording-pattern.mdresources.mdstimulus-catalog.mdstructured-events.mdturbo-morphing.mdwebhooks.md詳細
DHH Ruby/Rails スタイルガイド
DHH の哲学に従って Ruby と Rails のコードを書きます。何よりも 巧妙さより明瞭さ、設定より規約、開発者の幸福 を重視します。
クイックリファレンス
コントローラーアクション
- REST アクションは 7 つのみ:
index、show、new、create、edit、update、destroy - 新しい振る舞いが必要ですか? 独自アクションではなく、新しいコントローラーを作成します
- アクションの長さ:最大 1~5 行
- 空のアクションでも問題ありません:レンダリングは Rails の規約に任せます
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 キーワードの下で、プライベートメソッドを 1 段階インデントします。
private
def set_message
@message = Message.find(params[:id])
end
def message_params
params.require(:message).permit(:body)
end
モデル設計(ファットモデル)
モデルがビジネスロジック、認可、ブロードキャストを担います。
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 属性
リクエストコンテキストには Current を使い、current_user をあちこちに渡すことは避けます。
class Current < ActiveSupport::CurrentAttributes
attribute :user, :session
end
# Usage anywhere in app
Current.user.can_administer?(@message)
Ruby の構文に関する好み
DHH 独自のスタイルについては、一般的な Ruby スタイルを説明した ruby-coder スキルを参照してください。
# Symbol arrays with spaces inside brackets
before_action :set_message, only: %i[ show edit update destroy ]
# 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
クエリの最適化
map(&:name) より pluck(:name) を、messages.to_a.count より messages.count を優先します。処理はデータベースに任せます。
述語に StringInquirer を使う
文字列の enum には .inquiry を使い、読みやすい条件式にします。
class Event < ApplicationRecord
def action
super.inquiry
end
end
# Clean predicate methods
event.action.completed?
event.action.pending?
event.action.failed?
コントローラーのレスポンスパターン
本文のない更新には head :no_content を、作成には head :created を使います。すぐに失敗させる場合は、バングメソッド(create!、update!)を使います。
現在のユーザーのリソースには My:: 名前空間を使う
Current.user にスコープされたリソースには、My:: 名前空間を使います。
# 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
リソースは Current.user から暗黙的に決まるため、ID を使った index や show は必要ありません。
書き込み時に計算する
表示時ではなく、保存時にデータを加工します。
# 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
利点:ページネーションとキャッシュが可能になり、ビューの複雑さを軽減できます。
遅延読み込みには delegate を使う
関連を介した遅延読み込みを有効にするには、delegate を使います。
class Message < ApplicationRecord
belongs_to :session
delegate :user, to: :session
end
# Lazy loads user through session
message.user
命名規約
| 要素 | 規約 | 例 |
|---|---|---|
| セッターメソッド | set_ 接頭辞 | set_message、set_room |
| パラメータメソッド | {model}_params | message_params |
| 関連名 | 汎用的ではなく、意味のある名前 | user ではなく creator |
| スコープ | チェーン可能で説明的 | with_creator、page_before |
| 述語 | ? で終える | direct?、can_administer? |
| 現在のユーザーのリソース | My:: 名前空間 | My::ProfilesController |
Hotwire/Turbo パターン
ブロードキャストはモデルの責務です。
# In model
def broadcast_create
broadcast_append_to room, :messages, target: "messages"
end
Hotwire の詳しいパターンについては、hotwire-coder スキルを使ってください。
エラー処理
特定の例外を捕捉し、バングメソッドですぐに失敗させます。
def create
@message = @room.messages.create_with_attachment!(message_params)
@message.broadcast_create
rescue ActiveRecord::RecordNotFound
render action: :room_not_found
end
状態はレコードで管理する(ブール値ではなく)
ブール値のカラムではなく、データベースのレコードで状態を管理します。
# 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 の変換
独自アクションを、ネストされたリソースコントローラーに対応付けます。
| 独自アクション | REST リソース |
|---|---|
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
アーキテクチャに関する好み
| 従来の方法 | DHH の方法 |
|---|---|
| PostgreSQL | SQLite(シングルテナントの場合) |
| Redis + Sidekiq | Solid Queue |
| Redis キャッシュ | Solid Cache |
| Kubernetes | 単一の Docker コンテナ |
| サービスオブジェクト | ファットモデル |
| ポリシーオブジェクト(Pundit) | User モデルでの認可 |
| FactoryBot | フィクスチャ |
| ブール値の状態カラム | レコードで状態を管理 |
詳細なリファレンス
包括的なパターンと例については、以下を参照してください。
コアパターン
references/patterns.md- 説明付きのコードパターン集references/palkan-patterns.md- 名前空間付きモデルクラス、カウンターキャッシュ、モデルの整理順序、PostgreSQL の列挙型references/concerns-organization.md- モデル固有の Concern と共通 Concern、ファサードパターンreferences/delegated-types.md- STI の問題を避けるポリモーフィズムreferences/recording-pattern.md- 多様なコンテンツタイプを統一する抽象化references/filter-objects.md- PORO のフィルターオブジェクト、URL ベースの状態、テスト可能なクエリ構築references/database-patterns.md- UUIDv7、ハード削除、レコードとしての状態、カウンターキャッシュ、インデックス
Rails コンポーネント
references/activerecord-tips.md- ActiveRecord のクエリパターン、バリデーション、関連付けreferences/controllers-tips.md- コントローラーパターン、ルーティング、レート制限、フォームオブジェクトreferences/activestorage-tips.md- ファイルアップロード、添付ファイル、Blob の処理
Hotwire
references/hotwire-tips.md- Turbo Frames、Turbo Streams、ViewComponentsreferences/turbo-morphing.md- モーフィングパターンを使った Turbo 8 のページ更新references/stimulus-catalog.md- コピーしてすぐに使える Stimulus コントローラー(クリップボード、ダイアログ、ホットキーなど)- こちらも参照: 詳細なパターンについては、
hotwire-coder、stimulus-coder、viewcomponent-coderのスキルをご覧ください
フロントエンド
references/css-architecture.md- ネイティブ CSS のパターン(レイヤー、OKLCH、ネスト、ダークモード)
認証とマルチテナンシー
references/passwordless-auth.md- マジックリンク認証、セッション、アイデンティティモデルreferences/multi-tenancy.md- パスベースのテナンシー、Cookie のスコープ設定、テナント対応ジョブ
インフラストラクチャとインテグレーション
references/webhooks.md- 安全な Webhook 配信、SSRF 対策、リトライ戦略references/caching-strategies.md- ロシア人形キャッシュ、Solid Cache、キャッシュ分析references/config-tips.md- 設定、ログ記録、デプロイパターンreferences/structured-events.md- 構造化された可観測性のための Rails 8.1Rails.eventAPIreferences/resources.md- 参考資料と関連情報へのリンク
哲学のまとめ
- REST の純粋性: アクションは 7 つのみ。バリエーションには新しいコントローラーを作成
- ファットモデル: 認可、ブロードキャスト、ビジネスロジックをモデルに配置
- シンコントローラー: アクションは 1~5 行にし、複雑な処理は切り出す
- 設定より規約: 空のメソッド、暗黙のレンダリング
- 最小限の抽象化: 単純なケースではサービスオブジェクトを使わない
- Current attributes: あらゆる場所でスレッドローカルなリクエストコンテキストを使用
- Hotwire を優先: モデルレベルのブロードキャスト、Turbo Streams、Stimulus
- 読みやすいコード: 意味の明確な命名、小さなメソッド、コメント不要
成功の指標
次の条件を満たすコードは、DHH のスタイルに沿っています。
- コントローラーが CRUD の動詞をリソースに対応付けている(独自アクションなし)
- モデルが Concern を使って横断的な振る舞いを共有している
- 状態をブール型カラムではなくレコードで管理している
- 抽象化を最小限に保っている(不要なサービスオブジェクトなし)
- データベースを活用して解決している(Redis ではなく Solid Queue/Cache)
- すべてのインタラクティブな動作を Turbo/Stimulus で処理している
- 認可が User モデルに定義されている(
can_*?メソッド) - Current attributes がリクエストコンテキストを提供している
- スコープが命名規約に従っている(
chronologically、with_*など) - 属性の抽出に
mapではなくpluckを使っている - 現在のユーザーのリソースに
My::名前空間を使っている - データを表示時ではなく書き込み時に計算している