skills/controller-patterns/SKILL.md
Review and update existing Rails controllers and generate new controllers following professional patterns and best practices. Covers RESTful conventions, authorization patterns, proper error handling, and maintainable code organization.
npx skillsauth add rolemodel/rolemodel-skills controller-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Standard CRUD Controller:
class ResourcesController < ApplicationController
before_action :set_resource, only: %i[show edit update destroy]
def index
@resources = policy_scope(Resource)
end
def show; end
def new
@resource = authorize Resource.new
end
def create
@resource = authorize Resource.new(resource_params)
@resource.save ? redirect_to(@resource, notice: 'Successfully Created Resource') : render('new', status: :unprocessable_content)
end
def edit; end
def update
@resource.update(resource_params) ? redirect_to(@resource, notice: 'Successfully Updated Resource') : render('edit', status: :unprocessable_content)
end
def destroy
@resource.destroy
redirect_to resources_url, notice: 'Successfully Deleted Resource'
end
private
def set_resource
@resource = authorize Resource.find(params[:id])
end
def resource_params
params.expect(resource: %i[attr1 attr2])
end
end
Namespaced State Controller:
class Resources::StatesController < ApplicationController
before_action :set_resource
before_action :ensure_valid_state, only: :create
def create
@resource.activate!
redirect_to resources_path, notice: 'Resource activated.'
end
def destroy
@resource.deactivate!
redirect_to resources_path, notice: 'Resource deactivated.'
end
private
def set_resource
@resource = current_user.resources.find(params[:resource_id])
end
def ensure_valid_state
redirect_to(resources_path, alert: 'Invalid state') unless @resource.can_activate?
end
end
Need to add controller functionality?
│
├─ Standard CRUD operations (list, view, create, edit, delete)?
│ └─ Use: Standard RESTful Controller Pattern
│
├─ State transitions (submit, approve, activate, publish)?
│ └─ Use: Namespaced State Controller (create/destroy actions)
│
├─ Bulk operations (bulk submit, bulk delete)?
│ └─ Use: Namespaced Bulk Controller (create action only)
│
├─ Nested under parent resource?
│ └─ Use: Nested RESTful Controller Pattern
│
└─ Complex authorization rules?
└─ Add: Policy scopes and explicit authorization checks
Pattern: Authorize all resource interactions using authorize or policy_scope.
# Collections - use policy_scope
def index
@products = policy_scope(Product)
end
# New instances - authorize the class
def new
@product = authorize Product.new
end
# Existing instances - authorize in set method
def set_product
@product = authorize Product.find(params[:id])
end
Rules:
policy_scope() for collections (index)authorize ClassName.new() for new records (new, create)authorize in set_* methods for existing recordsPattern: Extract common setup logic with explicit action scoping.
# Resource loading (most common)
before_action :set_product, only: %i[show edit update destroy]
# Parent resource loading (nested)
before_action :set_company
before_action :set_employee, only: %i[show edit update destroy]
# State validation
before_action :ensure_pending, only: :create
before_action :ensure_stopped, only: :create
Rules:
only: or except:set_[resource], ensure_[state], require_[permission]State Validation Example:
def ensure_pending
return if @resource.pending?
redirect_to resources_path, alert: 'Must be pending.'
end
Index - List all resources:
def index
@resources = policy_scope(Resource)
end
Show - Display one resource:
def show
# Resource set via before_action
# Load scoped associations if needed
@related = policy_scope(@resource.related_items)
end
New - Form for new resource:
def new
@resource = authorize Resource.new
end
Create - Save new resource:
def create
@resource = authorize Resource.new(resource_params)
if @resource.save
redirect_to @resource, notice: 'Successfully Created Resource'
else
render :new, status: :unprocessable_content
end
end
Edit - Form for existing resource:
def edit
# Resource set via before_action
end
Update - Save changes to resource:
def update
if @resource.update(resource_params)
redirect_to @resource, notice: 'Successfully Updated Resource'
else
render :edit, status: :unprocessable_content
end
end
Destroy - Delete resource:
def destroy
@resource.destroy
redirect_to resources_url, notice: 'Successfully Deleted Resource'
end
Pattern: Define permitted attributes in private method.
def resource_params
params.expect(
resource: [
:simple_attr,
:another_attr,
nested_attrs: %i[id attr1 attr2 _destroy],
array_attrs: [],
multiple_ids: []
]
)
end
Rules:
params.expect(model: [...]){nested_attrs: %i[id attr _destroy]}{array_attr: []}:id for update, _destroy for deletion in nested attributes# Success - redirects (default 302, no status needed)
redirect_to @resource, notice: 'Success'
# Validation failure - render with unprocessable_content
render :new, status: :unprocessable_content # 422
render :edit, status: :unprocessable_content # 422
# Other statuses (rare in controllers)
head :no_content # 204
head :forbidden # 403
head :not_found # 404
Rules:
:unprocessable_content (422)Pattern: Consistent, user-friendly messaging.
# Success (notice:)
redirect_to @product, notice: 'Successfully Created Product'
redirect_to @product, notice: 'Successfully Updated Product'
redirect_to products_url, notice: 'Successfully Deleted Product'
# Errors (alert:)
redirect_to products_path, alert: 'Must be pending to submit.'
redirect_to products_path, alert: 'Cannot delete active product.'
Rules:
Successfully [Action] [Resource]notice: for successalert: for errors/warnings# Controllers
ProductsController < ApplicationController
Admin::ProductsController < Admin::BaseController
Products::SubmissionsController < ApplicationController
# Instance variables
@product, @user # Singular for one resource
@products, @users # Plural for collections
# Private methods
def set_product # Resource loading
def product_params # Strong parameters
def ensure_pending # State validation
def require_admin # Authorization check
class ProductsController < ApplicationController
before_action :set_product, only: %i[show edit update destroy]
def index
@products = policy_scope(Product)
end
def show; end
def new
@product = authorize Product.new
end
def create
@product = authorize Product.new(product_params)
if @product.save
redirect_to @product, notice: 'Successfully Created Product'
else
render :new, status: :unprocessable_content
end
end
def edit; end
def update
if @product.update(product_params)
redirect_to @product, notice: 'Successfully Updated Product'
else
render :edit, status: :unprocessable_content
end
end
def destroy
@product.destroy
redirect_to products_url, notice: 'Successfully Deleted Product'
end
private
def set_product
@product = authorize Product.find(params[:id])
end
def product_params
params.expect(product: %i[name description price])
end
end
class OrderItemsController < ApplicationController
before_action :set_order
before_action :set_order_item, only: %i[show edit update destroy]
def index
@order_items = policy_scope(@order.order_items)
end
def new
@order_item = authorize @order.order_items.build
end
def create
@order_item = authorize @order.order_items.build(order_item_params)
if @order_item.save
redirect_to [@order, @order_item], notice: 'Successfully Created Order Item'
else
render :new, status: :unprocessable_content
end
end
def update
if @order_item.update(order_item_params)
redirect_to [@order, @order_item], notice: 'Successfully Updated Order Item'
else
render :edit, status: :unprocessable_content
end
end
def destroy
@order_item.destroy
redirect_to order_order_items_url(@order), notice: 'Successfully Deleted Order Item'
end
private
def set_order
@order = authorize Order.find(params[:order_id])
end
def set_order_item
@order_item = authorize @order.order_items.find(params[:id])
end
def order_item_params
params.expect(order_item: %i[product_id quantity price])
end
end
| ❌ Anti-Pattern | ✅ Correct Pattern |
| --- | --- |
| @product = Product.new(product_params) | @product = authorize Product.new(product_params) |
| render :new, status: :unprocessable_entity | render :new, status: :unprocessable_content |
| render :new (on validation failure) | render :new, status: :unprocessable_content |
| @product = Product.new(params[:product]) | @product = Product.new(product_params) |
| params.require(:product).permit(:name) | params.expect(product: %i[name]) |
| redirect_to @product, notice: 'Product created!'<br>redirect_to @product, notice: 'Success!' | redirect_to @product, notice: 'Successfully Created Product' |
| before_action :set_product (no scope) | before_action :set_product, only: %i[show edit update destroy] |
| Custom action for state changes | Namespaced controller with RESTful actions |
Use When: Actions represent state transitions (submit/unsubmit, activate/deactivate, approve/reject) on a resource.
Pattern: Namespace under parent resource, use create and destroy for state changes.
# Controller: app/controllers/time_entries/submissions_controller.rb
class TimeEntries::SubmissionsController < ApplicationController
before_action :set_time_entry
before_action :ensure_valid_for_submission, only: :create
before_action :ensure_submitted, only: :destroy
def create
@time_entry.update!(status: :submitted, submitted_at: Time.current)
redirect_to time_entries_path, notice: 'Time entry submitted for approval.'
end
def destroy
@time_entry.update!(status: :pending, submitted_at: nil)
redirect_to time_entries_path, notice: 'Time entry unsubmitted.'
end
private
def set_time_entry
@time_entry = current_user.time_entries.find(params[:time_entry_id])
end
def ensure_valid_for_submission
return if @time_entry.pending? && @time_entry.stopped?
redirect_to time_entries_path, alert: 'Only stopped pending entries can be submitted.'
end
def ensure_submitted
return if @time_entry.submitted?
redirect_to time_entries_path, alert: 'Only submitted entries can be unsubmitted.'
end
end
# Routes
resources :time_entries do
resource :submission, only: [:create, :destroy], module: :time_entries
end
# Views
button_to time_entry_submission_path(@time_entry), method: :post # Submit
button_to time_entry_submission_path(@time_entry), method: :delete # Unsubmit
Benefits:
Use When: Operating on multiple records at once (bulk submit, bulk delete, bulk archive).
Pattern: Namespaced controller with only create action, validations in before_actions.
# Controller: app/controllers/time_entries/bulk_submissions_controller.rb
class TimeEntries::BulkSubmissionsController < ApplicationController
before_action :set_entries
before_action :ensure_entries_present
before_action :ensure_entries_valid
def create
@entries.update_all(status: TimeEntry.statuses[:submitted], submitted_at: Time.current)
redirect_to time_entries_path, notice: "#{@entries.count} #{'entry'.pluralize(@entries.count)} submitted."
end
private
def set_entries
ids = params[:time_entry_ids] || []
@entries = current_user.time_entries.where(id: ids)
end
def ensure_entries_present
return if @entries.any?
redirect_to time_entries_path, alert: 'No entries selected.'
end
def ensure_entries_valid
invalid = @entries.reject { |e| e.pending? && e.stopped? }
return if invalid.empty?
redirect_to time_entries_path, alert: 'Only stopped pending entries can be submitted.'
end
end
# Routes
resource :bulk_submissions, only: :create, module: :time_entries
# Views
form_with url: bulk_submissions_path, method: :post do |f|
# checkboxes for time_entry_ids[]
end
Use When: Showing a resource with multiple related collections.
def show
@active_projects = policy_scope(@company.projects.active)
@archived_projects = policy_scope(@company.projects.archived)
@team_members = policy_scope(@company.users)
end
Use When: Forms accept nested records (has_many associations).
def product_params
params.expect(
product: [
:name,
:description,
:price,
images_attributes: %i[id url alt_text _destroy],
variants_attributes: %i[id sku price stock_count _destroy],
tags: [],
category_ids: []
]
)
end
Key Points:
:id for updating existing nested records_destroy for deletion via nested attributes{ array_attr: [] } for simple arrays{ nested_attrs: %i[attr1 attr2] } for nested attribute hashesIdentify controller type:
Apply patterns:
authorize, policy_scope)Follow conventions:
ResourcesController or Resources::StatesControllerApplicationController@resource (singular), @resources (plural)set_resource, resource_paramsCheck for:
before_action with only:/except:params[] access):unprocessable_content on validation failuresSuccessfully [Action] [Resource]Priority order: Security (authorization, params) → RESTful patterns → Status codes → Messaging
testing
Verify what Ruby versions actually exist and install a specific Ruby via rbenv. Use BEFORE asserting that any Ruby version does or doesn't exist (e.g., "Ruby 4.0 isn't out yet", "the latest Ruby is 3.x", "Ruby X.Y.Z doesn't exist"). Also use when the user asks "what's the latest Ruby", "is Ruby X out", "does Ruby X.Y exist", "install Ruby", "switch to Ruby X", "what Ruby is installed", or mentions a specific Ruby version you're unsure about. Claude's training data may be out of date — run `check.sh` first.
development
Trace code through the stack — upward to entry points, downward to data, or laterally across boundaries. Use when the user asks "where does this get called from", "what calls this method", "trace this through the stack", "how does this request flow", "where does this data come from", "follow this through the code", or pastes/selects a piece of code and wants to understand where it fits in the larger system.
tools
Pick the single highest-priority unresolved Sentry issue and hand it off to a fixer skill. Use when triaging Sentry errors, running automated issue triage, or when asked to fix the top Sentry issue in a project.
tools
Find and fix issues from Sentry using MCP. Use when asked to fix Sentry errors, debug production issues, investigate exceptions, or resolve bugs reported in Sentry. Methodically analyzes stack traces, breadcrumbs, traces, and context to identify root causes.