From rails-simplifier
Simplifies Ruby on Rails code following 37signals patterns and the One Person Framework philosophy, preserving exact functionality. Focuses on recently modified code.
How this skill is triggered — by the user, by Claude, or both
Slash command
/rails-simplifier:simplifyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Simplify and refine Ruby on Rails code to enhance clarity, consistency, and maintainability **while preserving exact functionality**. Apply 37signals patterns and the One Person Framework philosophy to make Rails code more vanilla without altering its behavior.
Simplify and refine Ruby on Rails code to enhance clarity, consistency, and maintainability while preserving exact functionality. Apply 37signals patterns and the One Person Framework philosophy to make Rails code more vanilla without altering its behavior.
Deeper material lives in two references, read them when a refinement needs more than the summaries below:
references/philosophy.md — the why, distilled from the 37signals / Jorge Manrubia writing.references/patterns.md — the how, an implementation catalog of concrete patterns from the Fizzy codebase (routing, controllers, concerns, state records, auth, jobs, testing, caching, POROs).Three ideas drive every refinement. For the full treatment — quotes, examples, and the design principles behind them — read references/philosophy.md.
# ✅ GOOD: Natural, domain-oriented API (conceptual compression)
recording.incinerate
card.close
# ❌ BAD: Service/procedural style (expands complexity)
Recording::IncinerationService.execute(recording)
CardClosureService.new(card, user).call
"We strongly prefer the first form. It does a better job of hiding complexity, as it doesn't shift the burden of composition to the caller. It feels more natural, like plain English. It feels more Ruby." — Jorge Manrubia
You will analyze recently modified code and apply refinements that:
Never change what the code does — only how it does it. All original features, outputs, and behaviors must remain intact.
Every action should map to a CRUD verb. When something doesn't fit, create a new resource:
# ❌ BAD: Custom actions (expands controller complexity)
resources :cards do
post :close
post :reopen
post :archive
end
# ✅ GOOD: New resources for state changes (compresses to CRUD pattern)
resources :cards do
resource :closure # POST to close, DELETE to reopen
resource :archive # POST to archive, DELETE to unarchive
resource :goldness # POST to gild, DELETE to ungild
end
Controllers orchestrate; models contain business logic:
# ✅ GOOD: Controller just orchestrates
class Cards::ClosuresController < ApplicationController
include CardScoped
def create
@card.close # All logic in model — conceptual compression
respond_to do |format|
format.turbo_stream { render_card_replacement }
format.html { redirect_to @card, notice: t(".created") }
end
end
def destroy
@card.reopen
respond_to do |format|
format.turbo_stream { render_card_replacement }
format.html { redirect_to @card, notice: t(".destroyed") }
end
end
end
# ❌ BAD: Business logic in controller (complexity leak)
def create
@card.transaction do
@card.create_closure!(user: Current.user)
@card.events.create!(action: :closed)
NotificationMailer.card_closed(@card).deliver_later
end
end
Concerns must have "has trait" or "acts as" semantics. Self-contained with associations, scopes, callbacks, and methods:
# app/models/card/closeable.rb
module Card::Closeable
extend ActiveSupport::Concern
included do
has_one :closure, dependent: :destroy
scope :closed, -> { joins(:closure) }
scope :open, -> { where.missing(:closure) }
end
def close
transaction do
create_closure!(user: Current.user)
events.create!(action: :closed, creator: Current.user)
end
notify_watchers_of_closure
end
def reopen
closure&.destroy
events.create!(action: :reopened, creator: Current.user)
end
def closed?
closure.present?
end
def open?
!closed?
end
private
def notify_watchers_of_closure
watchers.each { |w| CardNotificationJob.perform_later(w, self, :closed) }
end
end
What concerns are NOT:
Model states as separate records to track who, when, and why — instead of boolean columns, which lose that context:
# ❌ BAD: Boolean columns (loses context)
class Card < ApplicationRecord
# closed: boolean
# closed_at: datetime
# closed_by_id: integer
end
# ✅ GOOD: State records (preserves full context)
class Card < ApplicationRecord
has_one :closure, dependent: :destroy
has_one :confirmation, dependent: :destroy
def closed?
closure.present?
end
def confirmed?
confirmation.present?
end
end
# app/models/closure.rb
class Closure < ApplicationRecord
belongs_to :card, touch: true
belongs_to :user, default: -> { Current.user }
# Fields: closed_at, reason (optional)
end
Benefits:
joins(:closure) vs where(closed: true))Pull a resource-scoping before_action and its shared render helpers into a small controller concern (e.g. CardScoped), so each thin controller just includes it. See the full concerns catalog — scoping, request context, timezone, Turbo flash — in references/patterns.md.
# ✅ GOOD: Natural verbs
card.close
card.reopen
card.gild
card.postpone
board.publish
booking.confirm
booking.cancel
# ❌ BAD: Procedural/setter style
card.set_closed(true)
card.update_status(:closed)
CardCloser.call(card)
card.closed?
card.open?
card.golden?
card.postponed?
booking.confirmed?
booking.cancelled?
# Derived from presence
def closed?
closure.present?
end
Closeable — can be closedPublishable — can be publishedWatchable — can be watchedConfirmable — can be confirmedCancellable — can be cancelledSchedulable — can be scheduled (shared across models)scope :chronologically, -> { order(created_at: :asc) }
scope :reverse_chronologically, -> { order(created_at: :desc) }
scope :alphabetically, -> { order(name: :asc) }
scope :active, -> { where(active: true) }
scope :upcoming, -> { where(starts_at: Time.current..) }
scope :today, -> { where(starts_at: Time.current.all_day) }
scope :preloaded, -> { includes(:creator, :tags) }
# ✅ GOOD
Time.current # Respects Rails timezone
Date.current # Respects Rails timezone
booking.starts_at.in_time_zone(account.timezone)
# ❌ BAD
Time.now # System timezone, inconsistent
Date.today # System timezone
# ✅ GOOD: Integer cents
add_column :services, :price_cents, :integer, default: 0, null: false
def price
price_cents / 100.0
end
def price=(value)
self.price_cents = (value.to_f * 100).round
end
# ❌ BAD: Float/Decimal
add_column :services, :price, :decimal # Precision issues
# ✅ GOOD: Always scope to current tenant
@bookings = current_account.bookings.upcoming
@booking = current_account.bookings.find(params[:id])
# ❌ BAD: Unscoped queries (security risk)
@booking = Booking.find(params[:id])
# ✅ GOOD: Eager load associations
@bookings = current_account.bookings
.includes(:client, :service, :user)
.upcoming
# ❌ BAD: N+1 queries
@bookings.each { |b| b.client.name } # N+1!
# ✅ GOOD: Rescue specific errors, log context
class Whatsapp::ReminderJob < ApplicationJob
retry_on Faraday::Error, wait: 5.minutes, attempts: 3
discard_on ActiveRecord::RecordNotFound
def perform(booking)
# Job logic
rescue StandardError => e
Rails.logger.error("Reminder failed", {
booking_id: booking.id,
error_class: e.class.name,
error_message: e.message
})
raise # Re-raise to trigger retry
end
end
# ✅ GOOD: Structured logging with context
Rails.logger.info("Booking created", {
booking_id: booking.id,
client_id: booking.client_id,
service: booking.service.name
})
# What to log: Auth events, booking lifecycle, external API calls, job execution, errors
# ❌ BAD: Logging sensitive data
Rails.logger.info("OTP: #{otp_code}") # Never log OTP
Rails.logger.info("Phone: #{user.phone}") # Mask: +52***5678
Rails.logger.info("Token: #{api_token}") # Never log tokens
Fixtures are loaded once, but Date.current is evaluated at test runtime. In parallel tests, this causes drift:
# ❌ BAD: Flaky test (date drift in parallel tests)
test "today scope" do
assert_includes Booking.today, bookings(:today_booking) # May fail near midnight!
end
# ✅ GOOD: Freeze time to match fixture
test "today scope" do
booking = bookings(:today_booking)
travel_to booking.date.to_time # Freeze to fixture's date
assert_includes Booking.today, booking
end
# ✅ GOOD: Freeze time when creating records
test "creates booking for today" do
freeze_time
post bookings_path, params: { booking: { date: Date.current } }
assert_equal Date.current, Booking.last.date
end
# ✅ GOOD: I18n everywhere
redirect_to @booking, notice: t(".created")
validates :starts_at, presence: { message: :blank } # Uses locale file
# ❌ BAD: Hardcoded strings
redirect_to @booking, notice: "Booking created!"
validates :starts_at, presence: { message: "can't be blank" }
| Scenario | Pattern |
|---|---|
| Default | Turbo Drive + Morph |
| List updates | Turbo Stream |
| Inline editing | Turbo Frame |
| Modals/dialogs | Turbo Frame |
| Multi-element updates | Turbo Stream |
def create
@booking = current_account.bookings.create!(booking_params)
respond_to do |format|
format.turbo_stream do
render turbo_stream: [
turbo_stream.prepend(:bookings, @booking),
turbo_stream.replace(:today_count, partial: "dashboard/today_count"),
turbo_stream_flash(notice: t(".created"))
]
end
format.html { redirect_to bookings_path, notice: t(".created") }
end
end
The turbo_stream_flash helper above comes from a small TurboFlash controller concern — see references/patterns.md for its definition alongside the rest of the concerns catalog.
| Anti-Pattern | Simplification | Why |
|---|---|---|
Service objects (*Service, *Interactor) | Rich model methods + concerns | Conceptual compression |
Custom controller actions (post :close) | CRUD resources (resource :closure) | Rails conventions |
Boolean state columns (closed: boolean) | State records (has_one :closure) | Track who/when/why |
| Fat controllers with business logic | Thin controllers, model methods | Single responsibility |
| Devise authentication | Rails 8 built-in auth | ~150 lines vs gem |
| Sidekiq/Redis | Solid Queue | One less service |
| React/Vue/JSON APIs | Hotwire (Turbo + Stimulus) | No build pipeline |
| RSpec + FactoryBot | Minitest + fixtures | Built-in, simpler |
Procedural naming (set_closed) | Verb methods (close) | Natural Ruby |
Time.now | Time.current | Timezone consistency |
| Float for money | Integer cents | Precision |
| Unscoped queries | Always scope to tenant | Security |
| N+1 queries | includes / preload | Performance |
| Hardcoded strings | I18n keys | Localization |
Date scope tests without travel_to | Freeze time to fixture date | Parallel test stability |
Time.now → replace with Time.currentincludestravel_toAvoid over-simplification that could:
Remember: The goal is conceptual compression — hiding complexity behind simple APIs, not eliminating necessary complexity.
Refine only code that has been recently modified or touched in the current session, unless explicitly instructed to review a broader scope. Apply refinements proactively right after Rails code is written or modified, without waiting for an explicit request. The goal is Rails code that follows the One Person Framework philosophy — simple enough that one developer can understand and maintain the entire system.
"The best code is the code you don't write. The second best is the code that's obviously correct."
"Vanilla Rails is plenty." — Jorge Manrubia, 37signals
npx claudepluginhub maquina-app/rails-claude-code --plugin rails-simplifierGenerates Ruby/Rails code following DHH's 37signals conventions: fat models, thin controllers, REST purity, Hotwire, and "clarity over cleverness."
Applies 37signals/DHH conventions to Ruby and Rails code: rich domain models, CRUD controllers, concerns, database-backed state, and minimal gems. Covers controllers, models, views (Turbo/Stimulus), architecture, testing (Minitest/fixtures), and code review.
Reviews and refactors Rails apps to Vanilla Rails style: thin controllers, rich domain models, no unnecessary service layers. For PR reviews, codebase analysis, simplification.