Use when writing Rails controllers, adding controller actions, or implementing state changes (close, archive, publish, assign) - enforces resource extraction instead of custom actions
View on GitHubZempTime/zemptime-marketplace
vanilla-rails
January 20, 2026
Select agents to install to:
npx add-skill https://github.com/ZempTime/zemptime-marketplace/blob/main/vanilla-rails/skills/controllers/SKILL.md -a claude-code --skill vanilla-rails-controllersInstallation paths:
.claude/skills/vanilla-rails-controllers/# Vanilla Rails Controllers
**Core principle:** State changes are resources. Model state changes with CRUD operations on resource controllers, never custom actions.
## When to Use
Use this skill when:
- Adding any state change to a model (close, archive, publish, assign, follow, etc.)
- Creating new controller actions
- Routing state transitions
**Red flags - STOP and extract a resource:**
- Adding `post :close`, `post :archive`, `patch :activate`
- Adding custom actions to existing resource routes
- Thinking "it's just a boolean toggle"
- Time pressure rationalizing "can refactor later"
## Resource Extraction Pattern
**The 37signals pattern:** Every state change becomes its own resource controller.
```ruby
# ❌ 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.
## Thin Controllers Calling Model Methods
Controllers delegate to intention-revealing model API. Keep business logic in models.
```ruby
# ❌ 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 destrIssues Found: