Use when writing Rails controllers, adding controller actions, or implementing state changes (close, archive, publish, assign) - enforces resource extraction instead of custom actions
/plugin marketplace add ZempTime/vanilla-rails/plugin install zemptime-vanilla-rails@ZempTime/vanilla-railsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Core principle: State changes are resources. Model state changes with CRUD operations on resource controllers, never custom actions.
Use this skill when:
Red flags - STOP and extract a resource:
post :close, post :archive, patch :activateThe 37signals pattern: Every state change becomes its own resource controller.
# ❌ BAD - custom actions (typical Rails tutorials)
resources :cards do
post :close
post :reopen
post :archive
post :unarchive
end
# ✅ GOOD - state as resource (37signals pattern)
resources :cards do
resource :closure, only: [:create, :destroy]
resource :archival, only: [:create, :destroy]
end
Why singular resource? Each card has at most ONE closure state, ONE archival state. Singular resource = no ID in URL.
Why only: [:create, :destroy]? Creating resource = entering state. Destroying resource = leaving state.
Controllers delegate to intention-revealing model API. Keep business logic in models.
# ❌ BAD - ActiveRecord calls in controller
class Cards::ArchivalsController < ApplicationController
def create
@card = Card.find(params[:id])
@card.update(archived: true) # Business logic in controller
redirect_to board_cards_path(@card.board)
end
end
# ✅ GOOD - delegate to model
class Cards::ArchivalsController < ApplicationController
include CardScoped # Sets @card from params
def create
@card.archive # Intention-revealing model method
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
def destroy
@card.unarchive
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
end
Model implements business logic:
module Card::Archivable
extend ActiveSupport::Concern
included do
has_one :archival, dependent: :destroy
scope :archived, -> { joins(:archival) }
scope :active, -> { where.missing(:archival) }
end
def archived?
archival.present?
end
def archive(user: Current.user)
unless archived?
transaction do
create_archival! user: user
track_event :archived, creator: user
end
end
end
def unarchive
archival&.destroy if archived?
end
end
Use params.expect() instead of params.require().permit():
# ❌ BAD - old Rails pattern
def card_params
params.require(:card).permit(:title, :description, :column_id)
end
# ✅ GOOD - Rails 8+ params.expect
def card_params
params.expect(card: [:title, :description, :column_id])
end
| State Change | Resource Name | create = | destroy = |
|---|---|---|---|
| Close/Reopen | closure | close | reopen |
| Archive/Unarchive | archival | archive | unarchive |
| Pin/Unpin | pinning or pin | pin | unpin |
| Publish/Unpublish | publication | publish | unpublish |
| Assign/Unassign | assignment | assign | unassign |
| Follow/Unfollow | subscription | subscribe | unsubscribe |
| Mark/Unmark as golden | goldness | gild | ungild |
| Excuse | Reality |
|---|---|
| "It's just a boolean toggle - resource is overkill" | Booleans ARE state. State changes ARE resources. Pattern applies. |
"I've seen post :close in Rails guides" | Standard Rails != 37signals Rails. Follow the codebase pattern. |
| "Creating a whole controller adds complexity" | 15 lines of controller is NOT complex. Clear, intention-revealing code. |
| "Time pressure - customer demo tomorrow" | Writing it wrong takes same time. Doing it right the first time is FASTER. |
| "Can refactor to resources later" | You won't. Do it right now while context is fresh. |
"Direct update() is more readable" | @card.archive is MORE readable than @card.update(archived: true). |
| "Association/table overhead for a boolean" | Tracks WHO and WHEN. Enables auditing, activity timeline, proper scopes. Worth it. |
| "Migration adds extra step" | One migration is NOT extra work. It's standard implementation. Do it. |
All of these mean: Extract resource, delegate to model. No exceptions.
Closing cards:
# routes.rb
resources :cards do
resource :closure
end
# app/controllers/cards/closures_controller.rb
class Cards::ClosuresController < ApplicationController
include CardScoped
def create
@card.close
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
def destroy
@card.reopen
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
end
# app/models/card/closeable.rb
module Card::Closeable
def close(user: Current.user)
unless closed?
transaction do
create_closure! user: user
track_event :closed, creator: user
end
end
end
def reopen(user: Current.user)
if closed?
transaction do
closure&.destroy
track_event :reopened, creator: user
end
end
end
end
When adding state change to a model:
resource :archival, only: [:create, :destroy]Cards::ArchivalsController)create (enter state) and destroy (leave state) actions@card.archive, not @card.update)has_one :archival association for state tracking.archived, .active) for queryingparams.expect() for strong parameters (if needed)State resources need a join table tracking when the state was entered and by whom:
# db/migrate/TIMESTAMP_create_card_archivals.rb
class CreateCardArchivals < ActiveRecord::Migration[8.0]
def change
create_table :card_archivals, id: :uuid do |t|
t.uuid :card_id, null: false
t.uuid :user_id
t.timestamps
t.index [:card_id], unique: true
end
end
end
Note: Table name follows pattern: card_ + plural of resource name
resource :closure → table closures (not card_closures)resource :goldness → table card_goldnesses (prefixed because goldness is namespaced)resource :archival → table card_archivals (prefixed for clarity)Naming formula:
resource :STATE_NAME (singular)Cards::STATE_NAMEsController (ALWAYS plural - ClosuresController, ArchivalsController)app/controllers/cards/STATE_NAMEs_controller.rbdef STATE_VERB (e.g., def archive, def close)has_one :STATE_NAMEcreate_table :card_STATE_NAMEs or create_table :STATE_NAMEsController template:
class Cards::STATEsController < ApplicationController
include CardScoped
def create
@card.enter_state
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
def destroy
@card.leave_state
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
end
Build robust backtesting systems for trading strategies with proper handling of look-ahead bias, survivorship bias, and transaction costs. Use when developing trading algorithms, validating strategies, or building backtesting infrastructure.