rails-best-practices-core について
日常的な Rails 開発の基本指針をまとめたスキルです。モデリング、ルーティング、認可、依存関係の選定、コードレビューを扱い、Rails の規約、明快なコード、データ安全性を重視します。
主な機能
- 01Rails の規約と組み込み機能を優先
- 02ドメインの振る舞いをモデルに配置
- 03レコードで状態と振る舞いを表現
- 04データベース制約で不変条件を保証
- 05REST ルーティングと認可のパターンを解説
- 06コードレビューの確認項目を提供
活用シーン
- 01Rails の変更に適した実装パターンを選定
- 02ドメインモデルとリソースルートを設計
- 03データ制約とテナント境界を確認
- 04依存関係の追加が必要か評価
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ marckohlbrugge/ 37signals-skills --skill rails-best-practices-coreインストール後、Claude Code で/rails-best-practices-coreを入力して読み込まれたか確認します。
詳細
Rails のベストプラクティス(コア)
Rails で作業する際のデフォルトの基準として使用してください。37signals のコードベース(Campfire、Fizzy)と DHH のレビューの傾向から抽出した内容です。
基本の方針
- 気の利いた抽象化より、明快で明示的なコードを優先します。抽象化は価値を示す必要があります。必要とするバリエーションを 3 つ以上挙げられないなら、インライン化してください。
- コントローラーは薄く保ち、ドメインの振る舞いはモデルに置きます。
- gem を追加する前に、Rails の規約や組み込み機能を優先します。
- 場当たり的なフラグではなく、ドメインの概念を使って状態と振る舞いをモデル化します。
- テナントやユーザーのデータは、所有権の境界を通じてスコープします。
- 強い不変条件にはデータベース制約を優先します。AR でバリデーションするのは、ユーザー向けのエラーメッセージが必要な場合に限ります。
- インターフェースは小さく保ちます。どこからも使われていないパブリックメソッドは追加しません。
- 高コストな読み取り時の組み立てより、書き込み時の計算を優先します(カウンターキャッシュ、委譲型、事前計算済みの集計、コールバックが不要な場合の
dependent: :delete_all)。 - モダンな Rails では、ストロングパラメーターに
params.expect(...)を使用します。 - クラッシュを許容します。bang メソッド(
create!)を使い、例外は境界で処理します。bang なしの対応メソッドが存在する場合にのみ!を使います。 - 症状ではなく根本原因を修正します(競合に対するリトライロジックより
enqueue_after_transaction_commitを使うなど)。 - 振る舞いの変更と同じ PR でテストを追加します。
モデリングのパターン
- ブール値ではなくレコードで状態を表します。
closed: booleanの代わりに、creatorとタイムスタンプを持つClosureレコードを作成します。誰がいつ行ったかを追加の作業なしで取得でき、スコープ設定も簡単です。
has_one :closure, dependent: :destroy
scope :closed, -> { joins(:closure) }
scope :open, -> { where.missing(:closure) }
- 大きなモデルは、機能にちなんだ名前(
Closeable、Watchable、Assignable)の concern に分割します。 各 concern は関連付け、スコープ、メソッドを備えた自己完結型にし、規模は約 50~150 行にします。ドメインの機能ごとに分割する場合は、モデルの名前空間内にネストしたモジュール(app/models/card/closeable.rbのCard::Closeable)を優先します。app/models/concerns/は、本当に複数モデルで共有される振る舞いに限って使います。プライベートメソッドしか含まない concern は決して抽出しません。 - PORO は
app/services/ではなくapp/models/に置きます。 表示用オブジェクト(Event::Description)、複雑な操作(SystemCommenter)、ビューコンテキストのまとまり(User::Filtering)などが該当します。これらはコントローラーではなく、モデルに隣接するものです。 - デフォルト値にはラムダを使います。
belongs_to :creator, class_name: "User", default: -> { Current.user }、belongs_to :account, default: -> { board.account }のようにします。 - リクエストコンテキストには Current 属性を使います(
Current.user、Current.account)。セッターは連鎖させます(sessionを設定するとidentityが解決され、さらに、そのアカウントのuserが解決されます)。 - コールバックはセットアップや後片付けに使い、ビジネスロジックには使いません。 コールバックの数は抑えます。
- 積極的に活用する Rails のショートカット:
normalizes(バリデーション前のデータクリーンアップ)、store_accessor(JSON カラム)、delegated_type(異種コレクション)、generates_token_for(有効期限付きの署名済みトークン)、enum :status, %w[drafted published].index_by(&:itself)による文字列 enum、after_save_commit、キャッシュ無効化のためのtouch: trueチェーン、delegate。 - 一括のドメイン操作には関連付けの拡張を使います。
has_manyのプロキシにgrant_to/reviseを定義します。一括作成にはinsert_allを使い、コールバックのない結合テーブルにはdependent: :delete_allを使います。 - 人にわかりやすい URL: 生の ID/UUID を公開するのではなく、テナントごとの
numberを使うようにto_paramをオーバーライドします。
命名
- 名前には時間をかけましょう。命名は設計です。
CardCloseよりClosure、UserReferenceよりMentionを選びます。 - 肯定形の名前を使います。
not_deletedではなくactive、not_hiddenではなくvisibleとします。 - 役割を表す意味のある関連付け名を使います。
belongs_to :userではなくbelongs_to :creator, class_name: "User"とします。 - 技術的な表現よりドメイン駆動の表現を優先します。
quota.over_limit?ではなくquota.depleted?とします。 - ビジネスに即したスコープを使います。SQL 風の
:without_popではなく、:active、:unassigned、:goldenとします。 - ドメイン用語を統一します。1 つの概念に対して
source/resource/containerを混在させません。
REST とルーティング
- すべてを CRUD として扱います。動詞を名詞に変えます。Close →
resource :closure(POST で閉じ、DELETE で再度開きます)、publish →resource :publication。カスタムのメンバーアクションは作りません。 - 親ごとに状態が 1 つだけ存在する場合は単数形の
resourceを使います。ネストされたコントローラー(Cards::ClosuresController)をグループ化するにはscope module:を使い、深い階層には浅いネストを使います。 - リソースのスコープを設定するコントローラー concern(
CardScopedはCurrent.user.accessible_cards.find_by!(...)を使って@cardを設定します)を、共有の Turbo レンダリングヘルパーも含めてネストされたコントローラー間で共有します。 - 親へのアンカー付きポリモーフィック URL を生成するには
resolve "Comment"を使います。 respond_toを使い、HTML/Turbo/JSON に同じコントローラーで対応します。API 専用の名前空間は作りません。
認可
- Pundit/CanCanCan は使いません。モデルにシンプルな述語メソッドを定義します(
card.editable_by?(user)、user.can_administer_board?(board))。 - コントローラーでチェックし(
head :forbidden unless ...)、権限の意味はモデルで定義します。 - 認証・認可の方針は、宣言的なコントローラーマクロで示します。
allow_unauthenticated_access、ensure_can_administer。
依存関係
gem を追加する前に、次の点を確認してください。標準の Rails で実現できますか? リポジトリ内に 50~150 行書くほうが、依存関係を追加するよりシンプルではありませんか? 多くの場合、Devise、Pundit、ViewComponent、RSpec、FactoryBot、Redis(Solid Queue/Cache/Cable はデータベースを使います)、サービスオブジェクト、フォームオブジェクト、デコレーター、GraphQL、SPA フレームワーク、Tailwind は追加せずに済みます。
レビューの優先順位
- 正確性とデータの安全性。
- マルチテナント/セキュリティの境界。
- 保守性と可読性。
- パフォーマンス上のボトルネック。
- スタイルと仕上がり。
必ず指摘する項目
- テナントを考慮するフローでの、スコープなしのレコード検索(
Comment.find(params[:id]))。 - 強い根拠のない新しい依存関係。
- SQL で行うべきインメモリのフィルタリング/ソート(および、
.pluck(:name)で済む場合の.map(&:name))。 - 単純なモデルメソッドで済む処理を置き換えるサービスオブジェクト。
- リソースとしてモデル化したほうが明確な場合の、RESTful ではないカスタムアクション。
- 誰がいつ状態を変更したかを記録するレコードの代わりに、真偽値の状態カラムを使うこと。
- HTTP キャッシュ(
fresh_when/etag)を使うフォーム付きページ。CSRF トークンが古くなり、422 エラーが発生します。 - 述語形式の API(StringInquirer/文字列 enum)が使える場合の、文字列によるステータスチェック(
status == "x")。 - 裏付けとなる一意インデックスがない
validates :x, uniqueness: true。 - 明示的な引数ではなく、暗黙のインスタンス変数に依存するヘルパー。
html_safe文字列へのエスケープされていない補間。先にエスケープします。"<b>#{h(input)}</b>".html_safe- 2~3 個のケースに対するメタプログラミング。メソッドをそのまま記述してください。
- private メソッドしかない concern。インライン化してください。
レビュー結果
- 重大度の高い指摘から始めます。
- 指摘ごとに、問題点、影響、具体的な修正方法を
file:lineの参照とともに記載します。 - 率直かつ実用的に書きます。「これは過剰設計です」だけでも、十分な指摘になります。
- 最後は
Ship itまたは優先順位を付けた短い修正リストで締めくくります。
コメント
Markdown 対応