Communitygithub.com

Railsパターン

Rails 7.1+ および 8.x アプリ向けのRuby on Railsフレームワークパターン。ディレクトリ構成、スリムなコントローラ、サービスオブジェクト、フォームオブジェクト、クエリオブジェクト、イディオマティックなActiveRecord、バックグラウンドジョブ、ViewComponent、Hotwire、Rails 8 Solidスタックをカバー。Railsアプリの構築・レビュー時、コントローラ・モデル・サービス・ジョブ・ビューを扱う際に使用。

Railsパターン とは?

このスキル(rails-patterns)は、Rails 7.1+ および 8.x アプリケーションの開発・レビューにおいて、モダンなベストプラクティスを適用するための一貫したフレームワークパターンを提供します。ディレクトリ構成の明確なガイドライン(`app/services/`、`app/forms/`、`app/queries/`など)を定義し、サービスオブジェクトによるスリムなコントローラ、複数モデルにまたがるフォーム処理のためのフォームオブジェクト、複雑・再利用性の高いクエリのためのクエリオブジェクトなど、具体的な実装パターンを示します。また、ViewComponentによるビューロジックのテスト容易性の向上、Hotwire(Turbo + Stimulus)によるサーバーサイドレンダリングの強化、Rails 8のSolid Queue / Solid Cache / Solid Cableスタックへの対応、N+1問題の防止、コールバックの適切な使用法など、実践的なコード例を豊富に含んでいます。特に、スキルはコミュニティで合意された「50モデル以降も保守可能な」パターンに焦点を当てており、どのパターンをいつ使用すべきかという判断は別のRubyパターンルールファイルでカバーされています。

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

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

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

ドキュメント

Railsパターン は何をしますか?

モダンなRuby on Railsアプリケーション(Rails 7.1+ および 8.x)のためのフレームワークパターン。Railsは設計上、意見が分かれるフレームワークですが、これらはコミュニティが「50モデルを超えても保守可能な」アプリケーションのために収束したパターンです。このスキルは「どのように(how)」実装するかを扱います。「何を(what)」そして「いつ(when)」—つまり、どのパターンを選ぶべきかの判断については、Rubyパターンルール(rules/ruby/patterns.md、このリポジトリ内、インストール先は rules/ecc/ruby/patterns.md)を参照してください。

アクティブにするタイミング

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

コアコンセプト

ディレクトリ契約

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

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

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

スリムなコントローラ

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

サービスオブジェクト

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

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

フォームオブジェクト

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

クエリオブジェクト

コントローラやサービス間で再利用されるActiveRecordクエリ、またはスコープには複雑すぎるクエリは、スコープを入力として受け取り合成可能なクエリオブジェクトに抽出します。目安: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 Load
@posts = Post.published.includes(:author)

includes を使用すると、Railsがpreloadとeager_loadを自動選択します。関連でフィルタリングする場合、個別のクエリには preload を、JOINを使用するには eager_load を強制します。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を渡してください。リトライにより配信は「at-least-once(少なくとも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
# このConcernをインクルードしているのはInvoiceのみ。横断的ではなく、
# 「スリムな」モデルに見せるためにモジュールに隠されたInvoice自身のロジックです。
# Invoiceに戻してください。

1つのクラスだけで使用されるConcernは単なるコードの移動であり、そのクラスに属するべきです。Concernは、関連のないモデル間で共有される横断的関心事のために予約してください。

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/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

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.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

関連スキル