controller-patterns
npx skills add https://github.com/rolemodel/rolemodel-skills --skill controller-patterns
Agent 安装分布
Skill 文档
Controller Best Practices
Purpose
This skill helps AI agents review existing Rails controllers and generate new controllers following professional patterns and best practices. It covers RESTful conventions, authorization patterns, proper error handling, and maintainable code organization that can be applied to any Rails application.
Context
This skill covers:
- Rails with RESTful conventions
- Authorization patterns (Pundit or similar)
- Strong parameters for security
- Proper HTTP status codes and flash messages
- Consistent naming conventions
- Error handling best practices
Best Practices
1. Authorization
Implement authorization checks for all actions that interact with resources. This example uses Pundit, but the pattern applies to any authorization framework (CanCanCan, ActionPolicy, etc.).
Key Principles:
- Authorize in all actions that interact with resources
- Use scoped queries for collection actions
- Authorize in both
set_*methods and create actions
# In index - scope collections to authorized records
def index
@products = policy_scope(Product)
# Or with CanCanCan: @products = Product.accessible_by(current_ability)
end
# In new/create - authorize new instances
def new
@product = authorize Product.new
end
def create
@product = authorize Product.new(product_params)
# ...
end
# In set method - authorize before any operation
def set_product
@product = authorize Product.find(params[:id])
# Or with CanCanCan: @product = Product.find(params[:id]); authorize! :read, @product
end
Why This Matters:
- Prevents unauthorized access to resources
- Provides a single, consistent authorization point
- Makes security audits easier
- Fails fast if authorization rules aren’t met
2. Before Actions
Use before_action to DRY up your controllers by extracting common setup logic.
before_action :set_product, only: %i[show edit update destroy]
before_action :set_company, only: %i[show edit update destroy]
Best Practices:
- Always use
only:orexcept:to be explicit about which actions are affected - Name methods descriptively:
set_[resource],require_admin,check_ownership - Order matters – list them in the order they should execute
- Keep before_action methods simple and focused
Common Before Actions:
# Resource loading
before_action :set_product, only: %i[show edit update destroy]
# Authorization checks
before_action :require_admin, only: %i[destroy]
before_action :require_ownership, only: %i[edit update destroy]
# State validation checks
before_action :ensure_pending, only: %i[create]
before_action :ensure_stopped, only: %i[create]
# Parent resource loading (for nested resources)
before_action :set_company
before_action :set_employee, only: %i[show edit update destroy]
State Validation Pattern:
Extract state validation into before_actions to keep controller actions focused:
private
def ensure_pending
return if @time_entry.pending?
redirect_to time_entries_path, alert: 'Only pending entries can be submitted.'
end
def ensure_stopped
return unless @time_entry.running?
redirect_to time_entries_path, alert: 'Cannot submit a running timer.'
end
3. RESTful Actions Structure
Index:
def index
@resources = policy_scope(Resource)
end
Show:
def show
# Set resource via before_action
# Load any associated data needed for the view
@related_items = policy_scope(@resource.related_items)
end
New:
def new
@resource = authorize Resource.new
end
Create:
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:
def edit
# Set resource via before_action
end
Update:
def update
if @resource.update(resource_params)
redirect_to @resource, notice: 'Successfully Updated Resource'
else
render 'edit', status: :unprocessable_content
end
end
Destroy:
def destroy
@resource.destroy
redirect_to resources_url, notice: 'Successfully Deleted Resource'
end
4. Private Methods
Set Method:
private
def set_resource
@resource = authorize Resource.find(params[:id])
end
Strong Parameters:
def resource_params
params.expect(resource: [
:attribute_one,
:attribute_two,
{ nested_attributes: %i[id attr1 attr2] },
{ array_attributes: [] },
])
end
5. HTTP Status Codes
Use semantic HTTP status codes to communicate the result of operations clearly.
Common Status Codes:
# Success (2xx)
render :show, status: :ok # 200 - Standard success
render :show, status: :created # 201 - Resource created (optional for create)
head :no_content # 204 - Success with no response body
# Client Errors (4xx)
render :new, status: :unprocessable_content # 422 - Validation failed
render json: {error: "Not found"}, status: :not_found # 404
head :forbidden # 403 - User lacks permission
head :unauthorized # 401 - Authentication required
Best Practices:
- Use
:unprocessable_content(422) for validation errors on create/update - Use standard redirects (302) for successful operations
- No explicit status needed for redirects (uses 302 by default)
- Turbo/Hotwire requires proper status codes for correct behavior
Example:
def create
@product = authorize Product.new(product_params)
if @product.save
redirect_to @product, notice: 'Successfully Created Product' # 302 redirect
else
render :new, status: :unprocessable_content # 422 for validation errors
end
end
6. Flash Messages
Use consistent, user-friendly flash messages for user feedback.
Message Patterns:
# Success messages (use notice:)
redirect_to @product, notice: 'Successfully Created Product'
redirect_to @product, notice: 'Successfully Updated Product'
redirect_to products_url, notice: 'Successfully Deleted Product'
# Error messages (use alert:)
redirect_to products_url, alert: 'Failed to delete product'
redirect_to @product, alert: 'Unable to process request'
# Info messages
redirect_to @product, notice: 'Email sent successfully'
Best Practices:
- Use
notice:for success messages - Use
alert:for error/warning messages - Keep messages concise and action-oriented
- Use consistent capitalization and phrasing
- Avoid technical jargon in user-facing messages
7. Naming Conventions
Follow Rails conventions for consistent, predictable code.
Controller Naming:
# Controller inherits from ApplicationController
class ProductsController < ApplicationController
# ...
end
# Nested namespaced controllers
class Admin::ProductsController < Admin::BaseController
# ...
end
Instance Variables:
# Singular for individual resources
@product, @user, @article, @order
# Plural for collections
@products, @users, @articles, @orders
# Related resources maintain context
@product_reviews, @user_orders
Private Method Names:
# Resource loading
def set_product
def set_user
# Strong parameters
def product_params
def user_params
# Authorization checks
def require_admin
def require_ownership
Examples
Simple CRUD Controller
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
Controller with Nested Resources
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
Review Checklist
When reviewing or generating controllers, verify:
- Controller inherits from
ApplicationController - All resource interactions use
authorizeorpolicy_scope -
before_actionis used appropriately withonly:parameter - All standard RESTful actions follow the pattern
- Strong parameters are properly defined in private method
- Nested attributes use proper symbols array syntax
- Array attributes use
[]notation - HTTP status
:unprocessable_contentis used for validation failures - Flash messages are consistent and user-friendly
- Redirects use resource path helpers
- Instance variables use appropriate singular/plural naming
- Private methods are properly defined and ordered
Anti-Patterns to Avoid
â Don’t skip authorization:
def create
@product = Product.new(product_params) # Missing authorize!
end
â Always authorize:
def create
@product = authorize Product.new(product_params)
end
â Don’t use incorrect status codes:
render :new, status: :unprocessable_entity # Wrong status
â Use correct status:
render :new, status: :unprocessable_content
â Don’t use inconsistent flash messages:
redirect_to @product, notice: 'Product created!'
redirect_to @product, notice: 'The product has been successfully created'
redirect_to @product, notice: 'Product was saved'
â Be consistent:
redirect_to @product, notice: 'Successfully Created Product'
redirect_to @product, notice: 'Successfully Updated Product'
redirect_to @product, notice: 'Successfully Deleted Product'
â Don’t forget strong parameters:
def create
@product = authorize Product.new(params[:product]) # Unsafe!
end
â Always use strong parameters:
def create
@product = authorize Product.new(product_params)
end
private
def product_params
params.expect(product: %i[name description price])
end
Advanced Patterns
RESTful Namespaced Controllers
For related actions on a resource, use namespaced controllers with standard RESTful actions (create and destroy) instead of custom actions. Organize controllers in a namespace folder matching the parent resource.
Anti-Pattern:
# â Custom action on main controller
class TimeEntriesController < ApplicationController
def submit
@time_entry.update!(status: :submitted)
redirect_to time_entries_path
end
end
# â Controller not namespaced properly
class UnsubmitTimeEntriesController < ApplicationController
def create
@time_entry.update!(status: :pending)
redirect_to time_entries_path
end
end
# routes.rb
resources :time_entries do
resource :submit_time_entry, only: [:create]
resource :unsubmit_time_entry, only: [:create]
end
# File structure
/controllers
/time_entries_controller.rb
/submit_time_entries_controller.rb
/unsubmit_time_entries_controller.rb
Better Pattern:
# â
Namespaced controller with create and destroy actions
class TimeEntries::SubmissionsController < ApplicationController
before_action :set_time_entry
before_action :ensure_pending, only: [:create]
before_action :ensure_stopped, 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_pending
return if @time_entry.pending?
redirect_to time_entries_path, alert: 'Only pending entries can be submitted.'
end
def ensure_stopped
return unless @time_entry.running?
redirect_to time_entries_path, alert: 'Cannot submit a running timer.'
end
def ensure_submitted
return if @time_entry.submitted?
redirect_to time_entries_path, alert: 'Only submitted entries can be unsubmitted.'
end
end
# routes.rb
resources :time_entries do
resource :submission, only: [:create, :destroy], module: :time_entries
end
# File structure
/controllers
/time_entries_controller.rb
/time_entries
/submissions_controller.rb
# View usage
button_to time_entry_submission_path(@time_entry), method: :post # submit
button_to time_entry_submission_path(@time_entry), method: :delete # unsubmit
When to Use:
- Actions that represent creating or destroying a conceptual sub-resource (submissions, subscriptions, approvals)
- Related actions that operate on the same parent resource
- Actions that change a primary state of a resource
- When you want to keep controllers focused and single-purpose
Benefits:
- Follows RESTful conventions (using
createanddestroyactions) - Groups related functionality under a clear namespace
- Cleaner file organization with namespace folders
- Easier to test and maintain
- Clear separation of concerns
- Standard routing patterns
- Single controller instead of multiple separate controllers
- Validation logic extracted to before_actions keeps controller actions focused
- Before_actions can be tested independently
Bulk Operations as Namespaced RESTful Controllers
Handle bulk operations in namespaced controllers using the create action with validation in before_actions:
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} time #{'entry'.pluralize(@entries.count)} submitted for approval."
end
private
def set_entries
entry_ids = params[:time_entry_ids] || []
@entries = current_user.time_entries.where(id: entry_ids)
end
def ensure_entries_present
return if @entries.any?
redirect_to time_entries_path, alert: 'No time entries selected.'
end
def ensure_entries_valid
invalid_entries = @entries.reject { |e| e.pending? && e.stopped? }
return if invalid_entries.empty?
redirect_to time_entries_path, alert: 'Only stopped pending entries can be submitted.'
end
end
# routes.rb
resource :bulk_submissions, only: [:create], module: :time_entries
# File structure
/controllers
/time_entries_controller.rb
/time_entries
/submissions_controller.rb
/bulk_submissions_controller.rb
# View usage
form_with url: bulk_submissions_path, method: :post do |f|
# form fields
end
Scoped Collections in Show Actions
When showing a resource with related collections, apply policy scopes:
def show
@active_projects = policy_scope(@company.projects.active)
@archived_projects = policy_scope(@company.projects.archived)
end
Multiple Nested Associations
Handle complex nested relationships:
def show
@reviews = policy_scope(@product.reviews)
@related_products = policy_scope(@product.category.products.where.not(id: @product.id))
end
Nested Attributes in Strong Parameters
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:
- Include
:idfor updating existing nested records - Include
_destroyto allow deletion of nested records - Use
[]for simple array attributes - Use
%i[...]for nested attributes hashes
Usage Instructions for AI Agents
When asked to review a controller:
- Check against the review checklist
- Identify any anti-patterns
- Suggest specific fixes with code examples
- Prioritize authorization and security issues
When asked to generate a new controller:
- Ask for the resource name and attributes if not provided
- Determine if it’s a simple or nested resource
- Follow the appropriate example pattern
- Include all standard RESTful actions unless specified otherwise
- Generate appropriate strong parameters based on attributes
- Ensure all authorization calls are in place