From vanilla-rails
Reviews and refactors Rails apps to Vanilla Rails style: thin controllers, rich domain models, no unnecessary service layers. For PR reviews, codebase analysis, simplification.
npx claudepluginhub iuhoay/skills --plugin vanilla-railsThis skill is limited to using the following tools:
Design and review Rails applications using the Vanilla Rails philosophy from 37signals/Basecamp.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Automates semantic versioning and release workflow for Claude Code plugins: bumps versions in package.json, marketplace.json, plugin.json; verifies builds; creates git tags, GitHub releases, changelogs.
Design and review Rails applications using the Vanilla Rails philosophy from 37signals/Basecamp.
This skill is informed by Fizzy - a production Rails application from 37signals.
Key Fizzy patterns:
@board.update!(board_params), @card.comments.create!(comment_params)include Closeable, Golden, Postponable, Watchablehas_one :closure, has_one :goldnessapp/services/ directoryVanilla Rails embraces Rails's built-in patterns and avoids premature abstraction:
Core Philosophy: Thin controllers that directly invoke a rich domain model. No service layers or other artifacts unless genuinely justified.
┌─────────────────────────────────────────┐
│ CONTROLLERS │
│ (Thin - HTTP concerns only) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ MODELS │
│ (Rich - Business logic lives here) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ ACTIVE RECORD / DATABASE │
└─────────────────────────────────────────┘
Core Rule: Don't add layers beyond what Rails provides unless you have a clear, justified reason.
/vanilla-rails:review for Vanilla Rails architecture review/vanilla-rails:analyze to identify over-engineering/vanilla-rails:simplify [goal] to plan refactoring toward Vanilla Rails| Anti-Pattern | Example | Fix |
|---|---|---|
| Fat service | 100-line service with domain logic | Move logic to model |
| Anemic model | Model with only attributes and associations | Add business methods |
| Controller as orchestrator | Controller calling multiple services | Call rich model methods |
| Premature service | Simple CRUD wrapped in service | Use plain Active Record |
| Service explosion | DoSomethingService for every action | Most should be model methods |
See Anti-Patterns Reference for complete list.
Services are justified when:
Fizzy uses plain objects for this:
# Multi-step signup with ActiveModel::Model
class Signup
include ActiveModel::Model
validates :email_address, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :full_name, presence: true
def create_identity
@identity = Identity.find_or_create_by!(email_address: email_address)
@identity.send_magic_link(for: :sign_up)
end
def complete
# Complex account creation with rollback handling
end
end
Fizzy uses ActiveRecord models for stateful operations:
# Stateful import with status tracking
class Account::Import < ApplicationRecord
enum :status, %w[ pending processing completed failed ].index_by(&:itself), default: :pending
def process(start: nil, callback: nil)
processing!
# Import logic with ZIP file handling
mark_completed
rescue => e
mark_as_failed
raise e
end
end
Prefer expanded conditionals over guard clauses (unless returning early at method start for non-trivial bodies).
# Bad - Guard clause
def todos_for_new_group
ids = params.require(:todolist)[:todo_ids]
return [] unless ids
@bucket.recordings.todos.find(ids.split(","))
end
# Good - Expanded conditional
def todos_for_new_group
if ids = params.require(:todolist)[:todo_ids]
@bucket.recordings.todos.find(ids.split(","))
else
[]
end
end
class methodspublic methods (with initialize at top)private methodsOrder methods vertically by invocation order to help readers follow code flow.
Model endpoints as REST operations. Don't add custom actions - introduce new resources instead.
# Bad
resources :cards do
post :close
post :reopen
end
# Good
resources :cards do
resource :closure
end
No newline under visibility modifiers; indent content under them.
class SomeClass
def some_method
# ...
end
private
def some_private_method
# ...
end
end
If a module only has private methods, mark private at top with extra newline but don't indent.
Write shallow job classes that delegate to domain models:
_later suffix for methods that enqueue jobs_now suffix for synchronous methods# Fizzy pattern: _later enqueues, _now does the work
module Event::Relaying
extend ActiveSupport::Concern
included do
after_create_commit :relay_later
end
def relay_later
Event::RelayJob.perform_later(self)
end
def relay_now
# actual implementation
end
end
class Event::RelayJob < ApplicationJob
def perform(event)
event.relay_now
end
end
Only use ! for methods with a counterpart without !. Don't use ! to flag destructive actions.
| Pattern | Use When | Reference |
|---|---|---|
| Plain Active Record | Simple CRUD, no coordination needed | plain-activerecord.md |
| Rich Model API | Complex behavior single model should own | rich-models.md |
| Concern | Shared behavior across models | concerns.md |
| Delegated Type | "Is-a" relationships with shared identity | delegated-type.md |
| Service/Form | Only when genuinely justified | when-to-use-services.md |
Run /vanilla:analyze to detect:
See examples/ directory for before/after comparisons showing the Vanilla Rails approach.
"Vanilla Rails is plenty." - DHH
Most applications don't need layers beyond what Rails provides. Embrace:
ActiveRecord models as the home of business logicResist:
For more depth, read the Vanilla Rails blog post.