Communitygithub.com

rails-patterns

Ruby on Rails Framework Patterns für Rails 7.1+ und 8.x-Apps. Deckt das Directory Contract, Skinny Controller mit Service Objects, Form Objects, Query Objects, idiomatisches ActiveRecord, Background Jobs, ViewComponent, Hotwire und den Rails 8 Solid Stack ab. Verwende es beim Erstellen oder Überprüfen von Rails-Apps, Controllern, Modellen, Services, Jobs oder Views.

Was ist rails-patterns?

Diese Skill-Datei definiert die empfohlenen Framework Patterns für moderne Ruby on Rails-Anwendungen (Rails 7.1+ und 8.x), die über die 50-Modell-Marke hinaus wartbar bleiben. Sie beschreibt eine klare Verzeichnisstruktur (Directory Contract) mit spezifischen Ordnern für Models, Controller, Views, Components, Services, Forms, Queries, Jobs und mehr – und rät von generischen Ordnern wie `app/lib/` ab. Die Patterns fördern Skinny Controller, die Geschäftslogik an Service Objects delegieren, sowie Form Objects für komplexe Formulare, Query Objects für wiederverwendbare Datenbankabfragen und ViewComponent als Alternative zu tief verschachtelten Partials. Für Background Jobs wird empfohlen, IDs statt Records zu übergeben und Jobs idempotent zu gestalten. Der Hotwire-Stack (Turbo + Stimulus) ist die Standard-Frontend-Lösung, ergänzt durch den Rails 8 Solid Stack (Solid Queue, Solid Cache, Solid Cable) als Redis-Alternative für moderate Durchsätze. Die Skill enthält detaillierte Code-Beispiele für Service-Objekte, Form-Objekte, Query-Objekte, ViewComponents, Hotwire-Integration und Best Practices wie N+1-Prävention, Counter Cache und akzeptable vs. inakzeptable Callbacks sowie Concerns.

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

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Rails Patterns

Framework Patterns für moderne Ruby on Rails-Anwendungen (Rails 7.1+ und 8.x). Rails ist von Natur aus meinungsstark; dies sind die Patterns, auf die sich die Community für Apps geeinigt hat, die auch jenseits der 50-Modell-Marke wartbar bleiben. Diese Skill beschreibt das „Wie“. Für das „Was“ und „Wann“ (die Entscheidungen, welches Pattern verwendet werden soll) siehe die Ruby-Patterns-Regeln – rules/ruby/patterns.md in diesem Repository, installiert als rules/ecc/ruby/patterns.md.

Wann aktivieren

  • Erstellen einer Rails-Anwendung (Full-Stack, API-only oder hybrid)
  • Überprüfen eines PRs, der app/ oder config/ betrifft
  • Generieren von Modellen, Controllern, Services oder Jobs
  • Eine Controller-Aktion wächst über ~10 Zeilen
  • Eine Model-Datei wächst über ~200 Zeilen
  • ActiveRecord-Abfragen tauchen in Controllern oder Views auf

Kernkonzepte

Das Verzeichnisvertrag (Directory Contract)

Rails-Apps folgen einer vorhersagbaren Struktur. Füge Verzeichnisse bewusst hinzu, nicht beiläufig.

app/
  models/         ActiveRecord-Modelle. Persistenz und Domänenlogik nahe an den Daten.
  controllers/    HTTP-Anfrageverarbeitung. Nur dünne Orchestrierung.
  views/          ERB-Vorlagen. Keine Geschäftslogik.
  components/     ViewComponent-Klassen. View-Logik, die Tests benötigt.
  services/       Service-Objekte. Mehrschrittige Geschäftsabläufe.
  forms/          Formularobjekte. Komplexe Formularverarbeitung über mehrere Modelle hinweg.
  queries/        Abfrageobjekte. Wiederverwendbare, zusammensetzbare ActiveRecord-Abfragen.
  jobs/           Hintergrundjobs. Asynchrone Arbeit via Solid Queue, Sidekiq oder GoodJob.
  mailers/        ActionMailer-Klassen.
  helpers/        View-Helfer. Nur winzige Präsentationslogik.
  policies/       Autorisierungsrichtlinien (falls Pundit verwendet wird). Optional.
  channels/       ActionCable-Kanäle für WebSocket-Arbeit.

Vermeide app/lib/, app/utils/, app/managers/. Wenn etwas nicht in die obigen Verzeichnisse passt, muss das Design meist überdacht werden, nicht ein neues Verzeichnis erstellt. Wirklich generischer Code kommt in lib/.

Schlanke Controller (Skinny Controllers)

Controller empfangen eine Anfrage, delegieren an das richtige Objekt und rendern eine Antwort. Geschäftslogik lebt woanders. (Gemäß den Ruby-Patterns-Regeln: Extrahiere in ein Service-Objekt, wenn der Controller mehrere Verantwortlichkeiten übernimmt.)

Service-Objekte

Der Standard für Geschäftsabläufe, die mehr als ein einzelnes Model-Save betreffen. Konventionen, die sie konsistent halten:

  • Namespace nach Domäne (Invoices::Create), nicht nach Suffix (InvoiceCreator).
  • Eine Klassenmethode .call delegiert an eine Instanz #call.
  • Gib ein Ergebnisobjekt (Result) zurück, keinen Boolean oder einen nackten Datensatz, damit der Aufrufer auf Erfolg, Fehler und den betroffenen Datensatz verzweigen kann.
  • Umschließe Mehrfachdatensatz-Schreibvorgänge in einer Transaktion.
  • Halte jedes Service-Objekt auf einen Zweck fokussiert (Invoices::Create, Invoices::MarkPaid), niemals Invoices::Manager.

Formularobjekte (Form Objects)

Wenn ein Formular mehrere Modelle umfasst oder Felder hat, die nicht auf Spalten abbilden, verwende ein Formularobjekt anstelle von verschachtelten Attributen oder virtuellen Attributen auf dem falschen Modell. Es verhält sich gegenüber der View wie ein Modell (form_with model: @form), während es Datensätze sauber zusammensetzt.

Abfrageobjekte (Query Objects)

Für ActiveRecord-Abfragen, die über Controller oder Services wiederverwendet werden oder zu komplex für einen Scope sind, extrahiere ein Abfrageobjekt, das einen Scope als Eingabe akzeptiert, sodass es sich zusammensetzen lässt. Faustregel: Ein Scope, der über drei verkettete Bedingungen hinauswächst oder Parameter annimmt, möchte ein Abfrageobjekt werden.

Hintergrundjobs (Background Jobs)

Lagere alles Langsame aus. (Gemäß den Ruby-Patterns-Regeln: Solid Queue für grüne Wiese Rails 8 mit moderatem Durchsatz; Sidekiq, wenn ausgereifte Beobachtbarkeit, hoher Durchsatz oder vorhandener Redis benötigt werden.) Unabhängig vom Adapter: Übergib IDs, nicht Datensätze, mache perform idempotent und setze retry_on/discard_on explizit.

ViewComponent statt Partials

Verwende für View-Logik mit bedingtem Rendern, mehr als zwei Argumenten oder Wiederverwendung an mehr als drei Stellen bevorzugt einen ViewComponent. Komponenten sind isoliert testbar und machen ihr Interface explizit; Partials mit tiefer bedingter Logik werden zu technischen Schulden.

Hotwire: Turbo und Stimulus

Der Standard-Rails-Frontend-Stack. (Gemäß den Ruby-Patterns-Regeln: Bevorzuge Hotwire für serverseitig gerenderte Apps; greife zu React/Vue nur, wenn die Interaktionskomplexität die Client-Oberfläche rechtfertigt.) Turbo Frames für teilweise Seitenaktualisierungen, Turbo Streams für serverseitig gesteuerte Aktualisierungen, Stimulus für kleine clientseitige Verhaltensweisen neben dem Markup.

Der Rails 8 Solid Stack

Rails 8 bringt datenbankgestützte Standardeinstellungen mit, die zuvor Redis benötigten: Solid Queue (Jobs), Solid Cache (Cache), Solid Cable (ActionCable). Der Kompromiss ist eine höhere Datenbanklast für eine weniger benötigte Infrastrukturkomponente; eine gute Wahl für moderaten Durchsatz, während Redis bei hoher Skalierung weiterhin die Nase vorn hat. Kamal ist das standardmäßige Docker-basierte Bereitstellungstool.

Codebeispiele

Schlanker Controller mit einem Service-Objekt

# Schlecht: Geschäftslogik im Controller
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: "Rechnung erstellt"
    else
      render :new
    end
  end
end

# Gut: Controller orchestriert, Service erledigt die Arbeit
class InvoicesController < ApplicationController
  def create
    result = Invoices::Create.call(params: invoice_params, user: current_user)

    if result.success?
      redirect_to result.invoice, notice: "Rechnung erstellt"
    else
      @invoice = result.invoice
      render :new, status: :unprocessable_entity
    end
  end
end

Das Service-Objekt

# app/services/invoices/create.rb
module Invoices
  class Create
    # Struct hält dies auf jedem Ruby ausführbar, das Rails 7.1 unterstützt.
    # Auf Ruby 3.2+ ist `Data.define(:success?, :invoice, :errors)` eine prägnantere
    # unveränderliche Alternative.
    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("Benachrichtigungsversand fehlgeschlagen für Rechnung #{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

Formularobjekt

# 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

Abfrageobjekt

# 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

Abfrageobjekte akzeptieren einen Scope, sodass sie sich zusammensetzen lassen: Invoices::Overdue.call(scope: current_user.invoices).

N+1-Vermeidung

# Schlecht: N+1 in der View, wenn sie post.author.name aufruft
@posts = Post.published

# Gut: Eager Loading
@posts = Post.published.includes(:author)

includes lässt Rails zwischen Preload und Eager Load wählen. Verwende preload für separate Abfragen, eager_load für einen JOIN, wenn auf die Assoziation gefiltert wird. Seit Rails 6.1 löst strict_loading bei versehentlichen Lazy Loads einen Fehler aus.

Counter Cache

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

post.comments_count wird zu einem Spaltenlesevorgang statt einem COUNT(*). Dieses Beispiel geht von einer neuen Tabelle aus; das Hinzufügen eines Counter Cache zu einer Tabelle, die bereits Zeilen enthält, erfordert ein Backfill, das hier außerhalb des Rahmens liegt.

Hintergrundjob-Struktur

Übergebe Datensatz-IDs, nicht Datensätze. Wiederholungen machen die Zustellung mindestens einmalig, daher muss jeder Job, der einen externen Dienst aufruft, idempotent sein – sonst dupliziert ein transienter Fehler nach erfolgreichem Remote-Aufruf den Effekt beim nächsten Versuch.

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

Der eindeutige Index macht dies sicher: Wenn zwei Versuche gleichzeitig laufen, lehnt die Datenbank den zweiten Einfügevorgang ab und Active Record löst den Konflikt innerhalb des Aufrufs auf, indem es die vorhandene Zeile zurückgibt. Dies geschieht ohne jobseitige Wiederholung – retry_on oben deckt nur AccountingApi::TransientError ab. Die Absicherung deckt das Fenster vor dem Remote-Aufruf ab; die Übergabe von idempotency_key an die API deckt das Fenster danach ab, sodass ein Absturz zwischen dem API-Aufruf und update! immer noch zu einem einzelnen Export führt.

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: Klicken auf Bearbeiten ersetzt nur diesen Frame %>
<%= turbo_frame_tag "invoice_#{@invoice.id}" do %>
  <div class="invoice">
    <%= link_to "Bearbeiten", 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)
  }
}

Akzeptable vs. inakzeptable Callbacks

# Akzeptabel: Reine Datennormalisierung
class User < ApplicationRecord
  before_validation :normalize_email

  private

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

# In ein Service verschieben: Nebenwirkungen in einem Callback versteckt
# class User < ApplicationRecord
#   after_create :send_welcome_email  # schwer zu deaktivieren, schwer zu testen
# end

Gutes vs. schlechtes Concern

# Gut: Wirklich querschnittlich, über mehrere nicht verwandte Modelle wiederverwendbar
# 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

# Schlecht: Ein "Concern", das von genau einem Modell verwendet wird und Logik enthält, die dorthin gehört
# app/models/concerns/invoice_calculations.rb
module InvoiceCalculations
  extend ActiveSupport::Concern

  def calculate_total
    line_items.sum(&:amount) + tax_total
  end
end
# Nur Invoice bindet dies ein. Es ist nicht querschnittlich; es ist Invoice eigene Logik,
# die für den Anschein eines "schlanken" Modells in einem Modul versteckt wird. Setze es zurück in Invoice.

Ein Concern, das nur von einer Klasse verwendet wird, ist nur Code-Verschiebung; es gehört in diese Klasse. Ein Concern sollte wie das SoftDeletable-Beispiel sein: Es fügt eine abgeschlossene, testbare, von den einbindenden Modellen unabhängige Funktionalität hinzu. Halte Dinge nah beieinander, bis es einen Grund gibt, sie zu trennen.

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.

Verwandte Skills