Communitygithub.com

patrones-rails

Patrones de framework Ruby on Rails para aplicaciones Rails 7.1+ y 8.x. Cubre el contrato de directorios, controladores ligeros con objetos de servicio, objetos de formulario, objetos de consulta, ActiveRecord idiomático, trabajos en segundo plano, ViewComponent, Hotwire y la pila Solid de Rails 8. Útil al construir o revisar aplicaciones Rails, controladores, modelos, servicios, trabajos o vistas.

¿Qué es patrones-rails?

Este skill define patrones de framework para aplicaciones Ruby on Rails modernas (Rails 7.1+ y 8.x). Proporciona una guía detallada para mantener aplicaciones mantenibles más allá de los 50 modelos. Cubre el contrato de directorios estándar (`app/models`, `app/controllers`, `app/views`, `app/services`, `app/forms`, `app/queries`, `app/jobs`, etc.) y promueve controladores ligeros que delegan la lógica de negocio a objetos de servicio. También incluye objetos de formulario para formularios complejos, objetos de consulta para consultas ActiveRecord reutilizables, trabajos en segundo plano con Solid Queue o Sidekiq, componentes ViewComponent sobre parciales, la pila Hotwire (Turbo y Stimulus) para frontend, y la pila Solid de Rails 8 que reemplaza Redis con bases de datos. El skill incluye ejemplos de código completos y buenas prácticas como prevención de N+1, caché de contador, y manejo seguro de callbacks y concerns. Está diseñado para usarse al construir aplicaciones Rails nuevas, revisar PRs que tocan `app/` o `config/`, o generar modelos, controladores, servicios o trabajos.

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

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Patrones Rails

Patrones de framework para aplicaciones Ruby on Rails modernas (Rails 7.1+ y 8.x). Rails es opinado por diseño; estos son los patrones en los que la comunidad ha convergido para aplicaciones que se mantienen mantenibles más allá de los 50 modelos. Este skill es el "cómo". Para el "qué" y el "cuándo" (las decisiones sobre qué patrón usar), consulta las reglas de patrones Ruby — rules/ruby/patterns.md en este repositorio, instalado como rules/ecc/ruby/patterns.md.

Cuándo activarlo

  • Construyendo una aplicación Rails (full-stack, solo API, o híbrida)
  • Revisando un PR que toque app/ o config/
  • Generando modelos, controladores, servicios o trabajos
  • Una acción de controlador crece más allá de ~10 líneas
  • Un archivo de modelo crece más allá de ~200 líneas
  • Las consultas ActiveRecord comienzan a aparecer en controladores o vistas

Conceptos centrales

El contrato de directorios

Las aplicaciones Rails siguen una estructura predecible. Agrega directorios deliberadamente, no casualmente.

app/
  models/         Modelos ActiveRecord. Persistencia y lógica de dominio cercana a los datos.
  controllers/    Manejo de solicitudes HTTP. Solo orquestación ligera.
  views/          Plantillas ERB. Sin lógica de negocio.
  components/     Clases ViewComponent. Lógica de vista que necesita pruebas.
  services/       Objetos de servicio. Operaciones de negocio multi-paso.
  forms/          Objetos de formulario. Manejo de formularios complejos a través de múltiples modelos.
  queries/        Objetos de consulta. Consultas ActiveRecord reutilizables y componibles.
  jobs/           Trabajos en segundo plano. Trabajo asíncrono mediante Solid Queue, Sidekiq o GoodJob.
  mailers/        Clases ActionMailer.
  helpers/        Helpers de vista. Solo lógica de presentación pequeña.
  policies/       Políticas de autorización (si se usa Pundit). Opcional.
  channels/       Canales ActionCable para trabajo con WebSocket.

Evita app/lib/, app/utils/, app/managers/. Si algo no encaja en los directorios anteriores, generalmente el diseño necesita replanteamiento, no un nuevo directorio. El código verdaderamente genérico va en lib/.

Controladores ligeros

Los controladores reciben una solicitud, delegan al objeto correcto y renderizan una respuesta. La lógica de negocio vive en otro lugar. (Según las reglas de patrones Ruby, extrae a un objeto de servicio cuando el controlador comienza a cargar con múltiples responsabilidades).

Objetos de servicio

El valor predeterminado para operaciones de negocio que tocan más de un solo guardado de modelo. Convenciones que los mantienen consistentes:

  • Espacio de nombres por dominio (Invoices::Create), no por sufijo (InvoiceCreator).
  • Un método de clase .call delega a una instancia #call.
  • Devuelve un objeto Resultado, no un booleano o un registro simple, para que el llamador pueda bifurcar según éxito, errores y el registro afectado.
  • Envuelve escrituras multi-registro en una transacción.
  • Mantén cada servicio de un solo propósito (Invoices::Create, Invoices::MarkPaid), nunca Invoices::Manager.

Objetos de formulario

Cuando un formulario abarca múltiples modelos o tiene campos que no se asignan a columnas, usa un objeto de formulario en lugar de atributos anidados o atributos virtuales en el modelo incorrecto. Se comporta como un modelo para la vista (form_with model: @form) mientras compone registros limpiamente.

Objetos de consulta

Para consultas ActiveRecord reutilizadas entre controladores o servicios, o demasiado complejas para un scope, extrae un objeto de consulta que acepte un scope como entrada para que se pueda componer. Regla general: un scope que crece más allá de tres condiciones encadenadas o comienza a tomar parámetros quiere ser un objeto de consulta.

Trabajos en segundo plano

Descarga cualquier cosa lenta. (Según las reglas de patrones Ruby, Solid Queue para Rails 8 nuevo con rendimiento modesto; Sidekiq cuando necesites observabilidad madura, alto rendimiento o Redis existente). Independientemente del adaptador: pasa IDs no registros, haz que perform sea idempotente y establece retry_on/discard_on explícitamente.

ViewComponent sobre parciales

Para lógica de vista con renderizado condicional, más de dos argumentos, o reutilización en más de tres lugares, prefiere un ViewComponent. Los componentes se pueden probar de forma aislada y muestran su interfaz explícitamente; los parciales con lógica condicional profunda se convierten en deuda.

Hotwire: Turbo y Stimulus

La pila frontend predeterminada de Rails. (Según las reglas de patrones Ruby, prefiere Hotwire para aplicaciones renderizadas por servidor; recurre a React/Vue solo cuando la complejidad de interacción justifique la superficie del cliente). Turbo Frames para actualizaciones parciales de página, Turbo Streams para actualizaciones impulsadas por servidor, Stimulus para comportamientos pequeños del lado del cliente junto al marcado.

La pila Solid de Rails 8

Rails 8 incluye valores predeterminados basados en base de datos que antes necesitaban Redis: Solid Queue (trabajos), Solid Cache (caché), Solid Cable (ActionCable). El equilibrio es más carga de base de datos a cambio de un componente de infraestructura menos; adecuado para rendimiento modesto, con Redis aún ganando a gran escala. Kamal es la herramienta de despliegue predeterminada basada en Docker.

Ejemplos de código

Controlador ligero con objeto de servicio

# Mal: lógica de negocio en el controlador
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: "Factura creada"
    else
      render :new
    end
  end
end

# Bien: el controlador orquesta, el servicio hace el trabajo
class InvoicesController < ApplicationController
  def create
    result = Invoices::Create.call(params: invoice_params, user: current_user)

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

El objeto de servicio

# app/services/invoices/create.rb
module Invoices
  class Create
    # Struct mantiene esto ejecutable en cada Ruby que Rails 7.1 soporta.
    # En Ruby 3.2+, `Data.define(:success?, :invoice, :errors)` es una
    # alternativa inmutable más concisa.
    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("La entrega de notificaciones falló para la factura #{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

Objeto de formulario

# 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

Objeto de consulta

# 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

Los objetos de consulta aceptan un scope, para que se compongan: Invoices::Overdue.call(scope: current_user.invoices).

Prevención de N+1

# Mal: N+1 en la vista cuando llama a post.author.name
@posts = Post.published

# Bien: carga ansiosa
@posts = Post.published.includes(:author)

includes permite a Rails elegir entre precarga y carga ansiosa. Usa preload para consultas separadas, eager_load para un JOIN al filtrar sobre la asociación. Desde Rails 6.1, strict_loading lanza una excepción en cargas perezosas accidentales.

Caché de contador

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

post.comments_count se convierte en una lectura de columna en lugar de un COUNT(*). Este ejemplo asume una tabla nueva; agregar un caché de contador a una tabla que ya tiene filas requiere un relleno, que está fuera del alcance aquí.

Forma del trabajo en segundo plano

Pasa IDs de registros, no registros. Los reintentos hacen que la entrega sea al menos una vez, por lo que cualquier trabajo que llame a un servicio externo debe ser idempotente; de lo contrario, un fallo transitorio después de que la llamada remota tenga éxito duplicará el efecto en el siguiente intento.

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

El índice único es lo que hace esto seguro: cuando dos intentos compiten, la base de datos rechaza la segunda inserción y Active Record resuelve el conflicto dentro de la llamada, devolviendo la fila existente. Eso sucede sin ningún reintento a nivel de trabajo — retry_on arriba cubre solo AccountingApi::TransientError. La guardia cubre la ventana antes de la llamada remota; pasar idempotency_key a la API cubre la ventana después, por lo que un bloqueo entre la llamada API y update! aún se resuelve en una sola exportación.

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: al hacer clic en Editar, reemplaza solo este frame %>
<%= turbo_frame_tag "invoice_#{@invoice.id}" do %>
  <div class="invoice">
    <%= link_to "Editar", 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 aceptables vs inaceptables

# Aceptable: normalización pura de datos
class User < ApplicationRecord
  before_validation :normalize_email

  private

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

# Mover a un servicio en su lugar: efectos secundarios ocultos en un callback
# class User < ApplicationRecord
#   after_create :send_welcome_email  # difícil de desactivar, difícil de probar
# end

Concern bueno vs concern malo

# Bueno: genuinamente transversal, reutilizable entre modelos no relacionados
# 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

# Mal: un "concern" usado por exactamente un modelo, que contiene lógica que le pertenece
# app/models/concerns/invoice_calculations.rb
module InvoiceCalculations
  extend ActiveSupport::Concern

  def calculate_total
    line_items.sum(&:amount) + tax_total
  end
end
# Solo Invoice incluye esto. No es transversal; es lógica propia de Invoice
# escondida en un módulo por la apariencia de un modelo "ligero". Colócalo de nuevo en Invoice.

Un concern usado por solo una clase es solo mover código; pertenece a esa clase. Un concern debería representar un comportamiento transversal genuino que múltiples modelos no relacionados comparten.

Prácticas de ActiveRecord

Reglas generales

  • Prefiere lazy loading (find, where, find_by) sobre SQL sin procesar.
  • Usa índices de base de datos compuestos para consultas con múltiples condiciones.
  • Agregados de base de datos (.count, .sum, .maximum,...) deberían reemplazar el conteo en memoria (records.length, records.sum(&:amount)) a menos que los registros ya estén cargados.
  • Los callbacks de modelo deberían manipular datos, no desencadenar efectos secundarios. Los efectos secundarios pertenecen a objetos de servicio (o trabajos).

Migraciones

  • Usa el DSL de migración de Rails, no SQL sin procesar.
  • Siempre agrega un índice para cada belongs_to.
  • Usa change en lugar de up/down a menos que la migración no sea reversible.
  • Usa null: false con un valor predeterminado cuando sea relevante para restringir nulos no intencionados.
# Bien: migración explícita y reversible
class CreateInvoices < ActiveRecord::Migration[7.1]
  def change
    create_table :invoices do |t|
      t.references :user, null: false, foreign_key: true
      t.integer :status, default: 0, null: false
      t.decimal :total, precision: 10, scale: 2, default: 0.0, null: false
      t.datetime :due_date
      t.timestamps
    end

    add_index :invoices, [:user_id, :status]
  end
end

Políticas de autorización (Pundit)

Cuando se use Pundit, las políticas viven bajo app/policies/.

# app/policies/invoice_policy.rb
class InvoicePolicy < ApplicationPolicy
  def show?
    user == record.user || user.admin?
  end

  def create?
    true
  end

  def update?
    user == record.user
  end
end
class InvoicesController < ApplicationController
  def show
    @invoice = Invoice.find(params[:id])
    authorize @invoice
  end
end

Comenzando

Para inicializar un nuevo proyecto Rails 8 con la pila Solid y Kamal:

rails new myapp --database=sqlite3 --skip-jbuilder

Para agregar Solid Queue a un proyecto Rails existente (7.1+):

bin/rails generate solid_queue:install

Para cambiar a ViewComponent:

bundle add view_component

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.

Skills relacionados