Communitygithub.com

rails-patterns

Ruby on Railsフレームワークのパターン集(Rails 7.1+ および 8.x 対応)。ディレクトリ規約、スキニーコントローラとサービスオブジェクト、フォームオブジェクト、クエリオブジェクト、イディオマティックなActive Record、バックグラウンドジョブ、ViewComponent、Hotwire、Rails 8 Solidスタックを網羅。Railsアプリ、コントローラ、モデル、サービス、ジョブ、ビューの構築・レビュー時に使用。

rails-patterns とは?

このスキルは、Rails 7.1+ および 8.x アプリケーション向けの、コミュニティで確立されたパターン群を提供します。スキニーコントローラ、サービスオブジェクト、フォームオブジェクト、クエリオブジェクトといったレイヤー分離の実践的な実装例を示し、コントローラやモデルの肥大化を防ぎます。また、HotwireやViewComponentを用いたモダンなフロントエンド構築、Rails 8のSolidスタック(Solid Queue, Solid Cache, Solid Cable)への対応、データベース駆動のバックグラウンドジョブ管理などもカバー。N+1対策やカウンターキャッシュ、お問い合わせ可能なコールバックとそうでないものの区別など、パフォーマンスと保守性を高める具体的なコード例が豊富に含まれています。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/rails-patterns

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Rails パターン集

モダンなRuby on Railsアプリケーション(Rails 7.1+ および 8.x)向けのフレームワークパターン集です。Railsは設計思想がはっきりしているフレームワークですが、ここではモデル数50を超えても保守しやすいアプリのためにコミュニティが合意したパターンをまとめています。このスキルは「どのように実装するか(How)」に焦点を当てています。「何を」「いつ」(どのパターンを選ぶべきか)については、Rubyパターンルールを参照してください。このリポジトリ内の rules/ruby/patterns.md (インストール先は rules/ecc/ruby/patterns.md)です。

アクティベートすべきタイミング

  • Railsアプリケーションを構築するとき(フルスタック、API専用、ハイブリッド)
  • app/ または config/ に影響するPRをレビューするとき
  • モデル、コントローラ、サービス、ジョブを生成するとき
  • コントローラのアクションが約10行を超えたとき
  • モデルファイルが約200行を超えたとき
  • コントローラやビューでActive Recordのクエリが出現し始めたとき

中核となる概念

ディレクトリ規約

Railsアプリは予測可能な構造に従います。ディレクトリは計画的に追加し、不用意に追加してはいけません。

app/
  models/         ActiveRecordモデル。データに近い永続化とドメインロジック。
  controllers/    HTTPリクエスト処理。薄いオーケストレーションのみ。
  views/          ERBテンプレート。ビジネスロジックは含めない。
  components/     ViewComponentクラス。テストが必要なビューロジック。
  services/       サービスオブジェクト。複数ステップのビジネス操作。
  forms/          フォームオブジェクト。複数モデルにまたがる複雑なフォーム処理。
  queries/        クエリオブジェクト。再利用可能で合成可能なActive Recordクエリ。
  jobs/           バックグラウンドジョブ。Solid Queue、Sidekiq、GoodJobによる非同期処理。
  mailers/        ActionMailerクラス。
  helpers/        ビューヘルパー。ごく小さなプレゼンテーションロジックのみ。
  policies/       認可ポリシー(Pundit使用時)。オプション。
  channels/       ActionCableチャンネル。WebSocket処理用。

app/lib/、app/utils/、app/managers/ は避けてください。何かが上記のディレクトリに適合しない場合、通常は新しいディレクトリを作るのではなく、設計を見直す必要があります。本当に汎用的なコードは lib/ に置きます。

スキニーコントローラ

コントローラはリクエストを受け取り、適切なオブジェクトに委譲し、レスポンスをレンダリングします。ビジネスロジックは他の場所に置きます(Rubyパターンルールに従い、コントローラが複数の責務を持ち始めたらサービスオブジェクトに抽出します)。

サービスオブジェクト

単一モデルの保存を超えるビジネス操作のデフォルト。一貫性を保つための規約:

  • ドメインで名前空間を作る(Invoices::Create)。サフィックス(InvoiceCreator)は使わない。
  • クラスメソッド .call がインスタンスの #call に委譲する。
  • Result オブジェクトを返す。真偽値や生のレコードは返さない。これにより呼び出し元は成功、エラー、影響を受けたレコードに基づいて分岐できる。
  • 複数レコードの書き込みはトランザクションでラップする。
  • 各サービスは単一目的に保つ(Invoices::Create、Invoices::MarkPaid)。決して Invoices::Manager にしない。

フォームオブジェクト

フォームが複数のモデルにまたがる場合や、カラムにマッピングできないフィールドがある場合は、ネストされた属性や間違ったモデル上の仮想属性ではなく、フォームオブジェクトを使用します。ビューに対してはモデルと同じように振る舞い(form_with model: @form)、レコードをきれいに構成します。

クエリオブジェクト

コントローラやサービス間で再利用されるActive Recordクエリ、またはスコープでは複雑すぎるクエリは、クエリオブジェクトに抽出します。このオブジェクトはスコープを入力として受け取り、合成可能にします。経験則:スコープが3つ以上の連鎖条件を超えるか、パラメータを取り始めたら、クエリオブジェクトにしたいタイミングです。

バックグラウンドジョブ

遅いものは何でもオフロードします(Rubyパターンルールに従い、グリーンフィールドのRails 8でスループットが控えめならSolid Queue、成熟した可観測性、高スループット、既存のRedisが必要ならSidekiq)。アダプタに関わらず:IDを渡しレコードは渡さない、perform を冪等にする、retry_on/discard_on を明示的に設定する。

ViewComponent > パーシャル

条件付きレンダリング、2つ以上の引数、または3箇所以上で再利用されるビューロジックには、パーシャルよりもViewComponentを推奨します。コンポーネントは単独でテスト可能で、インターフェースを明示的に公開します。深い条件ロジックを持つパーシャルは負債になります。

Hotwire: Turbo と Stimulus

デフォルトのRailsフロントエンドスタック(Rubyパターンルールに従い、サーバーレンダリングアプリにはHotwireを推奨。React/Vueはインタラクションの複雑さがクライアントサーフェスを正当化する場合のみ)。Turbo Framesは部分的なページ更新、Turbo Streamsはサーバー駆動の更新、Stimulusはマークアップの近くでの小さなクライアント側の振る舞いに使用。

Rails 8 Solid スタック

Rails 8は、以前はRedisが必要だったものをデータベースベースのデフォルトで出荷します:Solid Queue(ジョブ)、Solid Cache(キャッシュ)、Solid Cable(ActionCable)。トレードオフは、データベースの負荷が増える代わりにインフラコンポーネントが1つ減ることです。控えめなスループットに適しており、高スケールではRedisが依然として優位です。KamalはデフォルトのDockerベースのデプロイツールです。

コード例

スキニーコントローラとサービスオブジェクト

# 悪い例:コントローラにビジネスロジックがある
class InvoicesController < ApplicationController
  def create
    @invoice = Invoice.new(invoice_params)
    @invoice.user = current_user
    @invoice.line_items.build(invoice_params[:line_items])
    @invoice.tax_total = TaxCalculator.new(@invoice).calculate
    @invoice.total = @invoice.line_items.sum(&:amount) + @invoice.tax_total

    if @invoice.save
      InvoiceMailer.created(@invoice).deliver_later
      AccountingExportJob.perform_later(@invoice.id)
      redirect_to @invoice, notice: "請求書を作成しました"
    else
      render :new
    end
  end
end

# 良い例:コントローラはオーケストレーションし、サービスが作業を行う
class InvoicesController < ApplicationController
  def create
    result = Invoices::Create.call(params: invoice_params, user: current_user)

    if result.success?
      redirect_to result.invoice, notice: "請求書を作成しました"
    else
      @invoice = result.invoice
      render :new, status: :unprocessable_entity
    end
  end
end

サービスオブジェクト

# app/services/invoices/create.rb
module Invoices
  class Create
    # StructはRails 7.1がサポートするすべてのRubyで実行可能に保つ。
    # Ruby 3.2+ では、`Data.define(:success?, :invoice, :errors)` がより
    # 簡潔で不変な代替手段。
    Result = Struct.new(:success, :invoice, :errors, keyword_init: true) do
      def success?
        success
      end
    end

    def self.call(params:, user:)
      new(params: params, user: user).call
    end

    def initialize(params:, user:)
      @params = params
      @user = user
    end

    def call
      invoice = build_invoice
      ApplicationRecord.transaction do
        invoice.save!
      end
      begin
        send_notifications(invoice)
      rescue StandardError => e
        Rails.logger.error("請求書 #{invoice.id} の通知送信に失敗しました: #{e.message}")
      end
      Result.new(success: true, invoice: invoice, errors: nil)
    rescue ActiveRecord::RecordInvalid => e
      Result.new(success: false, invoice: e.record, errors: e.record.errors)
    end

    private

    attr_reader :params, :user

    def build_invoice
      invoice = user.invoices.new(params.except(:line_items))
      invoice.line_items.build(params[:line_items])
      invoice.tax_total = TaxCalculator.call(invoice)
      invoice.total = invoice.line_items.sum(&:amount) + invoice.tax_total
      invoice
    end

    def send_notifications(invoice)
      InvoiceMailer.created(invoice).deliver_later
      AccountingExportJob.perform_later(invoice.id)
    end
  end
end

フォームオブジェクト

# app/forms/signup_form.rb
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :email, :string
  attribute :password, :string
  attribute :company_name, :string
  attribute :terms_accepted, :boolean

  validates :email, presence: true, format: URI::MailTo::EMAIL_REGEXP
  validates :password, presence: true, length: { minimum: 12 }
  validates :company_name, presence: true
  validates :terms_accepted, acceptance: true

  attr_reader :user, :company

  def save
    return false unless valid?

    ApplicationRecord.transaction do
      @company = Company.create!(name: company_name)
      @user = @company.users.create!(email: email, password: password, role: :owner)
    end
    true
  rescue ActiveRecord::RecordInvalid => e
    errors.merge!(e.record.errors)
    false
  end
end

クエリオブジェクト

# app/queries/invoices/overdue.rb
module Invoices
  class Overdue
    def self.call(scope: Invoice.all, as_of: Time.current)
      new(scope: scope, as_of: as_of).call
    end

    def initialize(scope:, as_of:)
      @scope = scope
      @as_of = as_of
    end

    def call
      scope
        .where(status: :sent)
        .where(due_date: ..as_of)
        .where.not(id: paid_invoice_ids)
        .includes(:customer, :line_items)
    end

    private

    attr_reader :scope, :as_of

    def paid_invoice_ids
      Payment.where(created_at: ..as_of).pluck(:invoice_id)
    end
  end
end

クエリオブジェクトはスコープを受け取るため、合成可能です:Invoices::Overdue.call(scope: current_user.invoices)。

N+1対策

# 悪い例:ビューで post.author.name を呼び出すとN+1が発生
@posts = Post.published

# 良い例:eager loading
@posts = Post.published.includes(:author)

includes を使うとRailsがpreloadとeager_loadを選択します。関連付けでフィルタリングする場合は、preload で個別のクエリ、eager_load でJOINを強制します。Rails 6.1以降、strict_loading は不注意な遅延ロードに対して例外を発生させます。

カウンターキャッシュ

class Comment < ApplicationRecord
  belongs_to :post, counter_cache: true
end
add_column :posts, :comments_count, :integer, default: 0, null: false

post.comments_count は COUNT(*) ではなくカラムの読み取りになります。この例は新しいテーブルを想定しています。既に行があるテーブルにカウンターキャッシュを追加するにはバックフィルが必要であり、ここでは範囲外です。

バックグラウンドジョブの形状

レコードIDを渡し、レコードは渡さないでください。リトライにより配信は最低でも1回保証されるため、外部サービスを呼び出すジョブは冪等でなければなりません。そうでなければ、リモート呼び出しが成功した後の一時的な障害が、次の試行で効果を重複させます。

class AccountingExportJob < ApplicationJob
  queue_as :exports

  retry_on AccountingApi::TransientError, wait: :polynomially_longer, attempts: 5
  discard_on AccountingApi::PermanentError

  def perform(invoice_id)
    invoice = Invoice.find(invoice_id)
    export = AccountingExport.create_or_find_by!(
      invoice: invoice,
      idempotency_key: "invoice-export-#{invoice.id}-#{invoice.updated_at.to_i}"
    )
    return if export.completed_at?

    receipt = AccountingApi.export(invoice, idempotency_key: export.idempotency_key)
    export.update!(completed_at: Time.current, external_id: receipt.id)
  end
end
add_index :accounting_exports, :idempotency_key, unique: true

ユニークインデックスがこれを安全にします:2つの試行が競合した場合、データベースは2番目の挿入を拒否し、Active Recordが呼び出し内で競合を解決して既存の行を返します。これはジョブレベルのリトライなしで発生します。上記の retry_on は AccountingApi::TransientError のみをカバーします。ガードはリモート呼び出し前のウィンドウをカバーし、idempotency_key をAPIに渡すことでその後のウィンドウをカバーするため、API呼び出しと update! の間のクラッシュでも単一のエクスポートに解決されます。

ViewComponent

# app/components/invoice_status_badge_component.rb
class InvoiceStatusBadgeComponent < ViewComponent::Base
  STATUS_CLASSES = {
    draft: "bg-gray-100 text-gray-800",
    sent: "bg-blue-100 text-blue-800",
    paid: "bg-green-100 text-green-800",
    overdue: "bg-red-100 text-red-800"
  }.freeze

  def initialize(invoice:)
    @invoice = invoice
  end

  def call
    tag.span(@invoice.status.humanize, class: "rounded-full px-2 py-1 text-sm #{status_class}")
  end

  private

  def status_class
    STATUS_CLASSES.fetch(@invoice.status.to_sym, "bg-gray-100")
  end
end
<%= render InvoiceStatusBadgeComponent.new(invoice: @invoice) %>

Hotwire

<%# Turbo Frame: 「編集」をクリックするとこのフレームのみが置き換わる %>
<%= turbo_frame_tag "invoice_#{@invoice.id}" do %>
  <div class="invoice">
    <%= link_to "編集", edit_invoice_path(@invoice) %>
  </div>
<% end %>
<%# Turbo Stream: app/views/comments/create.turbo_stream.erb %>
<%= turbo_stream.append "comments", @comment %>
<%= turbo_stream.update "comment_form", partial: "form", locals: { comment: Comment.new } %>
// app/javascript/controllers/copy_to_clipboard_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["source"]

  copy() {
    navigator.clipboard.writeText(this.sourceTarget.value)
  }
}

許容可能なコールバックと許容不可能なコールバック

# 許容可能:純粋なデータの正規化
class User < ApplicationRecord
  before_validation :normalize_email

  private

  def normalize_email
    self.email = email.to_s.downcase.strip
  end
end

# サービスに移動すべき:副作用がコールバックに隠れている
# class User < ApplicationRecord
#   after_create :send_welcome_email  # オプトアウトが難しく、テストが難しい
# end

良いConcernと悪いConcern

# 良い:真に横断的で、無関係なモデル間で再利用可能
# app/models/concerns/soft_deletable.rb
module SoftDeletable
  extend ActiveSupport::Concern

  included do
    scope :active, -> { where(deleted_at: nil) }
    scope :deleted, -> { where.not(deleted_at: nil) }
  end

  def soft_delete! = update!(deleted_at: Time.current)
  def restore! = update!(deleted_at: nil)
end

# 悪い:ちょうど1つのモデルで使われる「concern」で、そのモデルに属するロジックを保持している
# app/models/concerns/invoice_calculations.rb
module InvoiceCalculations
  extend ActiveSupport::Concern

  def calculate_total
    line_items.sum(&:amount) + tax_total
  end
end
# このモジュールをincludeしているのはInvoiceのみ。これは横断的ではなく、Invoice自身のロジックを
# 「スリムなモデル」に見せかけるためにモジュールに隠したもの。Invoiceに戻すべき。

1つのクラスだけで使われるconcernは単なるコードの移動であり、そのクラスに属するべきです。concernは、複数の無関係なモデル間で真に横断的な振る舞いがある場合にのみ使用し、2つ以上のモデルで同じコードが必要になるまでは抽出を控えてください。

まとめ

  • 予測可能なディレクトリ構造を守る。app/lib/ や app/utils/ は避ける。
  • コントローラは薄く保つ。ビジネスロジックはサービスオブジェクトに抽出する。
  • サービスオブジェクトはドメインで名前空間を作り、Resultを返し、明示的に失敗できるようにする。
  • フォームオブジェクトで複数モデルのフォームと検証を扱う。
  • クエリオブジェクトで複雑なActive Recordクエリをカプセル化する。
  • ViewComponentをパーシャルよりも優先する。
  • バックグラウンドジョブは冪等にし、IDを渡し、明示的なリトライポリシーを設定する。
  • コールバックはデータの正規化のみに使い、副作用のためには使わない。
  • Concernは真に横断的な場合のみ使用し、そうでなければモデルにロジックを保持する。
  • Rails 8アプリでは、Solid Queue、Solid Cache、Solid Cableを使ってインフラストラクチャを簡素化する。

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

関連スキル