Communitygithub.com

rails-patterns

Patterns du framework Ruby on Rails pour les applications Rails 7.1+ et 8.x. Couvre le contrat de répertoire, les contrôleurs légers avec objets service, formulaires, requêtes, ActiveRecord idiomatique, jobs en arrière-plan, ViewComponent, Hotwire et la pile Solid de Rails 8. À utiliser lors de la création ou de la révision d'applications Rails, contrôleurs, modèles, services, jobs ou vues.

Qu'est-ce que rails-patterns ?

Cette compétence définit les patterns de conception standard pour les applications Ruby on Rails modernes (versions 7.1+ et 8.x), en se concentrant sur les pratiques que la communauté a adoptées pour maintenir la maintenabilité au-delà de 50 modèles. Elle fournit des directives structurées pour organiser le code dans une structure de répertoire prédictible, en mettant l'accent sur des contrôleurs légers qui délèguent la logique métier à des objets service, des objets formulaire pour la gestion de formulaires multi-modèles, des objets requête pour des requêtes ActiveRecord complexes et réutilisables, et des jobs en arrière-plan asynchrones via Solid Queue, Sidekiq ou GoodJob. La compétence recommande ViewComponent plutôt que les vues partielles traditionnelles pour une logique de vue testable, et adopte Hotwire (Turbo et Stimulus) comme pile frontend par défaut de Rails, avec Turbo Frames pour les mises à jour partielles de page et Turbo Streams pour les mises à jour pilotées par le serveur. Pour Rails 8, elle introduit la pile Solid (Solid Queue, Solid Cache, Solid Cable) qui remplace Redis par des solutions basées sur la base de données pour une infrastructure simplifiée, et Kamal pour le déploiement Docker. La compétence inclut des exemples de code détaillés montrant des implémentations correctes et incorrectes, couvrant les motifs de conception, la prévention des requêtes N+1, les caches de compteurs, les jobs idempotents, les ViewComponents, les interactions Hotwire, et les bonnes pratiques pour les callbacks et les préoccupations de modèle.

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Patterns Rails

Patterns framework pour les applications Ruby on Rails modernes (Rails 7.1+ et 8.x). Rails est conçu avec des opinions fortes ; voici les patterns que la communauté a adoptés pour les applications qui restent maintenables après la barre des 50 modèles. Cette compétence est le « comment ». Pour le « quoi » et le « quand » (les décisions sur quel pattern utiliser), voir les règles Ruby — rules/ruby/patterns.md dans ce dépôt, installé comme rules/ecc/ruby/patterns.md.

Quand activer

  • Création d'une application Rails (full-stack, API-only ou hybride)
  • Révision d'une PR qui modifie app/ ou config/
  • Génération de modèles, contrôleurs, services ou jobs
  • Une action de contrôleur dépasse ~10 lignes
  • Un fichier de modèle dépasse ~200 lignes
  • Des requêtes ActiveRecord apparaissent dans les contrôleurs ou les vues

Concepts fondamentaux

Le contrat de répertoire

Les applications Rails suivent une structure prévisible. Ajoutez des répertoires délibérément, pas par habitude.

app/
  models/         Modèles ActiveRecord. Persistance et logique métier proches des données.
  controllers/    Gestion des requêtes HTTP. Orchestration légère uniquement.
  views/          Templates ERB. Aucune logique métier.
  components/     Classes ViewComponent. Logique de vue nécessitant des tests.
  services/       Objets service. Opérations métier multi-étapes.
  forms/          Objets formulaire. Gestion de formulaires complexes sur plusieurs modèles.
  queries/        Objets requête. Requêtes ActiveRecord réutilisables et composables.
  jobs/           Jobs en arrière-plan. Travail asynchrone via Solid Queue, Sidekiq ou GoodJob.
  mailers/        Classes ActionMailer.
  helpers/        Helpers de vue. Logique de présentation minimale uniquement.
  policies/       Politiques d'autorisation (si utilisation de Pundit). Optionnel.
  channels/       Canaux ActionCable pour le travail WebSocket.

Évitez app/lib/, app/utils/, app/managers/. Si quelque chose ne correspond pas aux répertoires ci-dessus, la conception nécessite généralement une refonte, pas un nouveau répertoire. Le code véritablement générique va dans lib/.

Contrôleurs légers

Les contrôleurs reçoivent une requête, délèguent à l'objet approprié et renvoient une réponse. La logique métier réside ailleurs. (Selon les règles Ruby, extrayez dans un objet service lorsque le contrôleur commence à porter plusieurs responsabilités.)

Objets service

La valeur par défaut pour les opérations métier qui touchent plus qu'une simple sauvegarde de modèle unique. Conventions pour les garder cohérents :

  • Namespace par domaine (Invoices::Create), pas par suffixe (InvoiceCreator).
  • Une méthode de classe .call délègue à une instance #call.
  • Renvoie un objet Result, pas un booléen ou un enregistrement brut, afin que l'appelant puisse bifurquer sur le succès, les erreurs et l'enregistrement concerné.
  • Encapsule les écritures multi-enregistrements dans une transaction.
  • Gardez chaque service à usage unique (Invoices::Create, Invoices::MarkPaid), jamais Invoices::Manager.

Objets formulaire

Lorsqu'un formulaire couvre plusieurs modèles ou a des champs qui ne correspondent pas aux colonnes, utilisez un objet formulaire plutôt que des attributs imbriqués ou des attributs virtuels sur le mauvais modèle. Il se comporte comme un modèle pour la vue (form_with model: @form) tout en composant proprement les enregistrements.

Objets requête

Pour les requêtes ActiveRecord réutilisées dans les contrôleurs ou les services, ou trop complexes pour un scope, extrayez un objet requête qui accepte un scope en entrée afin qu'il se compose. Règle empirique : un scope qui dépasse trois conditions chaînées ou commence à prendre des paramètres doit devenir un objet requête.

Jobs en arrière-plan

Déléguez tout ce qui est lent. (Selon les règles Ruby, Solid Queue pour un nouveau projet Rails 8 avec un débit modeste ; Sidekiq lorsque vous avez besoin d'observabilité mature, d'un débit élevé ou d'un Redis existant.) Quel que soit l'adaptateur : passez des ID, pas des enregistrements, rendez perform idempotent et définissez retry_on/discard_on explicitement.

ViewComponent plutôt que des vues partielles

Pour une logique de vue avec rendu conditionnel, plus de deux arguments ou une réutilisation dans plus de trois endroits, préférez un ViewComponent. Les composants sont testables de manière isolée et exposent leur interface explicitement ; les vues partielles avec une logique conditionnelle profonde deviennent une dette technique.

Hotwire : Turbo et Stimulus

La pile frontend par défaut de Rails. (Selon les règles Ruby, préférez Hotwire pour les applications rendues côté serveur ; optez pour React/Vue uniquement lorsque la complexité des interactions justifie la surface client.) Turbo Frames pour les mises à jour partielles de page, Turbo Streams pour les mises à jour pilotées par le serveur, Stimulus pour les petits comportements côté client à côté du balisage.

La pile Solid de Rails 8

Rails 8 intègre des valeurs par défaut basées sur la base de données qui nécessitaient auparavant Redis : Solid Queue (jobs), Solid Cache (cache), Solid Cable (ActionCable). Le compromis est une charge de base de données plus élevée pour un composant d'infrastructure en moins ; un bon choix pour un débit modeste, Redis restant gagnant à grande échelle. Kamal est l'outil de déploiement Docker par défaut.

Exemples de code

Contrôleur léger avec un objet service

# Mauvais : logique métier dans le contrôleur
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: "Invoice created"
    else
      render :new
    end
  end
end

# Bon : le contrôleur orchestre, le service fait le travail
class InvoicesController < ApplicationController
  def create
    result = Invoices::Create.call(params: invoice_params, user: current_user)

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

L'objet service

# app/services/invoices/create.rb
module Invoices
  class Create
    # Struct permet de l'exécuter sur toutes les versions de Ruby que Rails 7.1 supporte.
    # Sur Ruby 3.2+, `Data.define(:success?, :invoice, :errors)` est une
    # alternative immuable plus concise.
    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("Notification dispatch failed for invoice #{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

Objet formulaire

# 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

Objet requête

# 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

Les objets requête acceptent un scope, donc ils se composent : Invoices::Overdue.call(scope: current_user.invoices).

Prévention des requêtes N+1

# Mauvais : N+1 dans la vue lorsqu'elle appelle post.author.name
@posts = Post.published

# Bon : chargement eager
@posts = Post.published.includes(:author)

includes laisse Rails choisir entre preload et eager_load. Forcez preload pour des requêtes séparées, eager_load pour un JOIN lors du filtrage sur l'association. Depuis Rails 6.1, strict_loading lève une exception en cas de chargement paresseux accidentel.

Cache de compteur

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

post.comments_count devient une lecture de colonne au lieu d'un COUNT(*). Cet exemple suppose une nouvelle table ; l'ajout d'un cache de compteur à une table qui contient déjà des lignes nécessite un backfill, qui sort du cadre de ce document.

Format du job en arrière-plan

Passez les ID des enregistrements, pas les enregistrements eux-mêmes. Les tentatives de rejeu rendent la livraison au moins une fois, donc tout job qui appelle un service externe doit être idempotent — sinon un échec transitoire après que l'appel distant a réussi dupliquera l'effet lors de la tentative suivante.

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

L'index unique est ce qui rend cela sûr : lorsque deux tentatives sont en concurrence, la base de données rejette la deuxième insertion et Active Record résout le conflit à l'intérieur de l'appel, renvoyant la ligne existante. Cela se produit sans aucune tentative de rejeu au niveau du job — retry_on ci-dessus ne couvre que AccountingApi::TransientError. La garde couvre la fenêtre avant l'appel distant ; le passage de idempotency_key à l'API couvre la fenêtre après celui-ci, de sorte qu'un crash entre l'appel API et update! aboutit toujours à un seul export.

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 : cliquer sur Edit remplace uniquement ce frame %>
<%= turbo_frame_tag "invoice_#{@invoice.id}" do %>
  <div class="invoice">
    <%= link_to "Edit", 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)
  }
}

Callbacks acceptables vs inacceptables

# Acceptable : normalisation pure des données
class User < ApplicationRecord
  before_validation :normalize_email

  private

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

# À déplacer dans un service à la place : effets de bord cachés dans un callback
# class User < ApplicationRecord
#   after_create :send_welcome_email  # difficile à désactiver, difficile à tester
# end

Bon concern vs mauvais concern

# Bon : véritablement transversal, réutilisable dans des modèles sans lien
# 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

# Mauvais : un "concern" utilisé par un seul modèle, contenant une logique qui lui appartient
# app/models/concerns/invoice_calculations.rb
module InvoiceCalculations
  extend ActiveSupport::Concern

  def calculate_total
    line_items.sum(&:amount) + tax_total
  end
end
# Seul Invoice inclut cela. Ce n'est pas transversal ; c'est la propre logique d'Invoice
# cachée dans un module pour l'apparence d'un modèle "léger". Remettez-le dans Invoice.

Un concern utilisé par une seule classe n'est qu'un déplacement de code ; il appartient à cette classe. Un concern devrait rassembler un comportement qui est partagé par au moins deux modèles sans lien de parenté. Le critère décisif est la réutilisation, pas la taille du fichier.

Gestion des dépendances

Règles pour le Gemfile

  • Utilisez gem "pundit" plutôt qu'un contrôle d'accès maison — couvre les tests d'autorisation

Configuration de l'outillage

RuboCop

# .rubocop.yml
require:
  - rubocop-rails
  - rubocop-rspec
  - rubocop-performance

AllCops:
  TargetRubyVersion: 3.2
  NewCops: enable

Rails:
  Enabled: true

L'essentiel est rubocop-rails pour les règles spécifiques à Rails, rubocop-rspec pour la cohérence des tests et rubocop-performance pour les optimisations de performances. Ajustez TargetRubyVersion à la version de Ruby du projet (3.2 pour les nouveaux projets Rails 8).

Variables d'environnement

  • DATABASE_URL — chaîne de connexion à la base de données
  • RAILS_MASTER_KEY — clé de déchiffrement des credentials (ne jamais commiter)
  • REDIS_URL — uniquement lorsque Redis est encore utilisé
  • SOLID_QUEUE_CONCURRENCY — (Rails 8, Solid Queue) nombre de workers parallèles

Variables d'environnement clés

VariableButRequise
DATABASE_URLChaîne de connexion à la base de donnéesOui
RAILS_MASTER_KEYDéchiffrement des credentials RailsOui, en production
REDIS_URLConnexion Redis (si utilisé)Conditionnelle
SOLID_QUEUE_CONCURRENCYParallelisme des jobs Solid QueueNon, défaut à 5

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.

Skills associés