Communitygithub.com

padrões-rails

Padrões do framework Ruby on Rails para aplicações Rails 7.1+ e 8.x. Abrange o contrato de diretórios, controladores enxutos com objetos de serviço, objetos de formulário, objetos de consulta, ActiveRecord idiomático, jobs em segundo plano, ViewComponent, Hotwire e a stack Solid do Rails 8. Use ao construir ou revisar aplicações Rails, controladores, modelos, serviços, jobs ou visões.

O que é padrões-rails?

Este skill define um conjunto opinativo e testado de padrões arquiteturais para aplicações modernas Ruby on Rails (Rails 7.1+ e 8.x). Ele descreve como estruturar o código de forma previsível e sustentável, especialmente para aplicações que ultrapassam cerca de 50 modelos. O foco principal é o 'contrato de diretórios', que organiza o código em pastas específicas (models, controllers, views, services, forms, queries, jobs, etc.), evitando diretórios genéricos como 'app/lib/'. O skill detalha a implementação de controladores enxutos que delegam lógica de negócios para objetos de serviço, o uso de objetos de formulário para cenários complexos e objetos de consulta para consultas ActiveRecord reutilizáveis. Ele também cobre práticas recomendadas para jobs em segundo plano (usando IDs em vez de registros, idempotência), substituição de parciais por ViewComponent para lógica de visão testável, e a integração com Hotwire (Turbo e Stimulus) como a stack frontend padrão do Rails. Para o Rails 8, o skill promove a stack Solid (Solid Queue, Solid Cache, Solid Cable) como alternativa ao Redis para cenários de throughput moderado. Por fim, ele fornece exemplos de código detalhados mostrando versões boas e ruins, incluindo prevenção de N+1, uso de cache de contagem, definição de concerns realmente reutilizáveis, e a implementação segura de jobs com chaves de idempotência.

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

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Padrões Rails

Padrões de framework para aplicações modernas Ruby on Rails (Rails 7.1+ e 8.x). Rails é opinativo por design; estes são os padrões nos quais a comunidade convergiu para aplicações que se mantêm sustentáveis após a marca de 50 modelos. Este skill é o "como". Para o "o quê" e "quando" (as decisões sobre qual padrão usar), veja as regras de padrões Ruby — rules/ruby/patterns.md neste repositório, instaladas como rules/ecc/ruby/patterns.md.

Quando Ativar

  • Construindo uma aplicação Rails (full-stack, somente API, ou híbrida)
  • Revisando um PR que modifica app/ ou config/
  • Gerando modelos, controladores, serviços ou jobs
  • Uma ação de controlador cresce além de ~10 linhas
  • Um arquivo de modelo cresce além de ~200 linhas
  • Consultas ActiveRecord começam a aparecer em controladores ou visões

Conceitos Principais

O contrato de diretórios

Aplicativos Rails seguem uma estrutura previsível. Adicione diretórios deliberadamente, não casualmente.

app/
  models/         Modelos ActiveRecord. Persistência e lógica de domínio próximas aos dados.
  controllers/    Manipulação de requisições HTTP. Orquestração enxuta apenas.
  views/          Templates ERB. Nenhuma lógica de negócio.
  components/     Classes ViewComponent. Lógica de visão que precisa de testes.
  services/       Objetos de serviço. Operações de negócio com múltiplas etapas.
  forms/          Objetos de formulário. Manipulação complexa de formulários em vários modelos.
  queries/        Objetos de consulta. Consultas ActiveRecord reutilizáveis e compostas.
  jobs/           Jobs em segundo plano. Trabalho assíncrono via Solid Queue, Sidekiq, ou GoodJob.
  mailers/        Classes ActionMailer.
  helpers/        Helpers de visão. Apenas lógica de apresentação pequena.
  policies/       Políticas de autorização (se usando Pundit). Opcional.
  channels/       Canais ActionCable para trabalho WebSocket.

Evite app/lib/, app/utils/, app/managers/. Se algo não se encaixa nos diretórios acima, o design geralmente precisa ser repensado, não de um novo diretório. Código verdadeiramente genérico vai em lib/.

Controladores enxutos

Controladores recebem uma requisição, delegam ao objeto certo e renderizam uma resposta. A lógica de negócio vive em outro lugar. (De acordo com as regras de padrões Ruby, extraia para um objeto de serviço quando o controlador começar a carregar múltiplas responsabilidades.)

Objetos de serviço

O padrão para operações de negócio que tocam mais do que um único salvamento de modelo. Convenções que os mantêm consistentes:

  • Namespace por domínio (Invoices::Create), não por sufixo (InvoiceCreator).
  • Um método de classe .call delega para uma instância #call.
  • Retorna um objeto Resultado, não um booleano ou um registro simples, para que o chamador possa ramificar em sucesso, erros e o registro afetado.
  • Envolva escritas de vários registros em uma transação.
  • Mantenha cada serviço com um único propósito (Invoices::Create, Invoices::MarkPaid), nunca Invoices::Manager.

Objetos de formulário

Quando um formulário abrange vários modelos ou tem campos que não mapeiam para colunas, use um objeto de formulário em vez de atributos aninhados ou atributos virtuais no modelo errado. Ele se comporta como um modelo para a visão (form_with model: @form) enquanto compõe registros de forma limpa.

Objetos de consulta

Para consultas ActiveRecord reutilizadas em controladores ou serviços, ou muito complexas para um escopo, extraia um objeto de consulta que aceita um escopo como entrada para que ele componha. Regra de ouro: um escopo que cresce além de três condições encadeadas ou começa a receber parâmetros quer ser um objeto de consulta.

Jobs em segundo plano

Terceirize qualquer coisa lenta. (De acordo com as regras de padrões Ruby, Solid Queue para Rails 8 novo com throughput modesto; Sidekiq quando você precisa de observabilidade madura, alto throughput, ou Redis existente.) Independentemente do adaptador: passe IDs não registros, torne perform idempotente e defina retry_on/discard_on explicitamente.

ViewComponent sobre parciais

Para lógica de visão com renderização condicional, mais de dois argumentos, ou reuso em mais de três lugares, prefira um ViewComponent. Componentes são testáveis isoladamente e expõem sua interface explicitamente; parciais com lógica condicional profunda se tornam dívida.

Hotwire: Turbo e Stimulus

A stack frontend padrão do Rails. (De acordo com as regras de padrões Ruby, prefira Hotwire para aplicações renderizadas no servidor; recorra a React/Vue apenas quando a complexidade da interação justificar a superfície do cliente.) Turbo Frames para atualizações parciais de página, Turbo Streams para atualizações orientadas pelo servidor, Stimulus para pequenos comportamentos no lado do cliente junto à marcação.

A stack Solid do Rails 8

Rails 8 fornece padrões baseados em banco de dados que antes precisavam de Redis: Solid Queue (jobs), Solid Cache (cache), Solid Cable (ActionCable). A troca é mais carga no banco de dados para um componente de infraestrutura a menos; uma boa escolha para throughput modesto, com Redis ainda vencendo em alta escala. Kamal é a ferramenta de deploy padrão baseada em Docker.

Exemplos de Código

Controlador enxuto com um objeto de serviço

# Ruim: lógica de negócio no 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: "Fatura criada"
    else
      render :new
    end
  end
end

# Bom: controlador orquestra, serviço faz o trabalho
class InvoicesController < ApplicationController
  def create
    result = Invoices::Create.call(params: invoice_params, user: current_user)

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

O objeto de serviço

# app/services/invoices/create.rb
module Invoices
  class Create
    # Struct mantém isso executável em todos os Rubies que o Rails 7.1 suporta.
    # No Ruby 3.2+, `Data.define(:success?, :invoice, :errors)` é uma
    # alternativa imutável mais 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("Falha no envio de notificação para fatura #{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 formulário

# 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

Objetos de consulta aceitam um escopo, então eles compõem: Invoices::Overdue.call(scope: current_user.invoices).

Prevenção N+1

# Ruim: N+1 na visão quando chama post.author.name
@posts = Post.published

# Bom: eager load
@posts = Post.published.includes(:author)

includes permite que o Rails escolha entre preload e eager_load. Force preload para consultas separadas, eager_load para um JOIN ao filtrar na associação. Desde o Rails 6.1, strict_loading levanta erro em carregamentos lentos acidentais.

Cache de contagem

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

post.comments_count se torna uma leitura de coluna em vez de um COUNT(*). Este exemplo assume uma nova tabela; adicionar um cache de contagem a uma tabela que já tem linhas requer um backfill, que está fora do escopo aqui.

Formato de job em segundo plano

Passe IDs de registros, não registros. As tentativas tornam a entrega pelo menos uma vez, então qualquer job que chame um serviço externo deve ser idempotente — caso contrário, uma falha transitória após a chamada remota bem-sucedida duplicará o efeito na próxima tentativa.

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

O índice único é o que torna isso seguro: quando duas tentativas competem, o banco de dados rejeita a segunda inserção e o Active Record resolve o conflito dentro da chamada, retornando a linha existente. Isso acontece sem qualquer nova tentativa no nível do job — retry_on acima cobre apenas AccountingApi::TransientError. A guarda cobre a janela antes da chamada remota; passar idempotency_key para a API cobre a janela depois dela, para que uma falha entre a chamada da API e update! ainda resolva para uma única exportação.

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: clicar em Edit substitui apenas 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 aceitáveis vs inaceitáveis

# Aceitável: normalização pura de dados
class User < ApplicationRecord
  before_validation :normalize_email

  private

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

# Mover para um serviço: efeitos colaterais escondidos em um callback
# class User < ApplicationRecord
#   after_create :send_welcome_email  # difícil de optar por não usar, difícil de testar
# end

Concern bom vs concern ruim

# Bom: genuinamente transversal, reutilizável em modelos não 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

# Ruim: um "concern" usado por exatamente um modelo, segurando lógica que pertence a ele
# app/models/concerns/invoice_calculations.rb
module InvoiceCalculations
  extend ActiveSupport::Concern

  def calculate_total
    line_items.sum(&:amount) + tax_total
  end
end
# Apenas Invoice inclui isso. Não é transversal; é a própria lógica de Invoice
# escondida em um módulo para a aparência de um modelo "enxuto". Coloque de volta em Invoice.

Um concern usado por apenas uma classe é apenas mover código; ele pertence àquela classe. Um concern deve capturar um padrão transversal a múltiplos modelos não relacionados. Se você tem apenas um modelo (como SoftDeletable apenas em Invoice e Comment), o concern é válido. Se você tem apenas um modelo, o código deve estar diretamente nele.

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.

Habilidades Relacionadas