controller-patterns
Rails Controller Patterns
Quick Reference
When to Use This Skill
- Generating new Rails controllers
- Reviewing existing controllers for best practices
- Implementing RESTful actions (index, show, new, create, edit, update, destroy)
- Adding authorization with Pundit
- Handling nested resources
- Implementing state transitions (submissions, approvals, activations)
- Bulk operations on resources
Core Patterns at a Glance
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
Decision Tree
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
Essential Patterns
1. Authorization (Pundit)
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)authorizeinset_*methods for existing records- Never skip authorization on resource operations
2. Before Actions
Pattern: 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:
- Always use
only:orexcept: - Name descriptively:
set_[resource],ensure_[state],require_[permission] - Order matters - execute in declaration order
- Keep methods focused on single responsibility
State Validation Example:
def ensure_pending
return if @resource.pending?
redirect_to resources_path, alert: 'Must be pending.'
end
3. RESTful Action Patterns
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
4. Strong Parameters
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:
- Use
params.expect(model: [...]) - Nested attributes:
{nested_attrs: %i[id attr _destroy]} - Arrays:
{array_attr: []} - Include
:idfor update,_destroyfor deletion in nested attributes
5. HTTP Status Codes
# 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:
- Redirects never need explicit status
- Failed validations:
:unprocessable_content(422) - Turbo requires proper status codes for error handling
6. Flash Messages
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:
- Format:
Successfully [Action] [Resource] - Use
notice:for success - Use
alert:for errors/warnings - Keep concise and action-oriented
- Title case for resource names
7. Naming Conventions
# 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
Complete 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
Nested Resource Controller
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
Common Mistakes
| ❌ 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!'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 |
Advanced Patterns
Namespaced State Controllers
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:
- RESTful (uses standard create/destroy actions)
- Clear file organization (controllers/time_entries/submissions_controller.rb)
- Validation extracted to before_actions
- Single controller handles both transitions
- Easy to test
Bulk Operation Controllers
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
Scoped Collections in Show
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
Complex Nested Attributes
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:
- Include
:idfor updating existing nested records - Include
_destroyfor deletion via nested attributes - Use
{ array_attr: [] }for simple arrays - Use
{ nested_attrs: %i[attr1 attr2] }for nested attribute hashes
Agent Instructions
Generating New Controllers
-
Identify controller type:
- Standard CRUD → Use Simple CRUD pattern
- State transitions → Use Namespaced State pattern
- Bulk operations → Use Bulk Operation pattern
- Nested resource → Use Nested Resource pattern
-
Apply patterns:
- Start with appropriate template
- Add authorization (
authorize,policy_scope) - Define strong parameters
- Add before_actions with explicit scoping
- Use correct status codes and flash messages
-
Follow conventions:
- Name:
ResourcesControllerorResources::StatesController - Inherit from:
ApplicationController - Instance variables:
@resource(singular),@resources(plural) - Private methods:
set_resource,resource_params
- Name:
Reviewing Existing Controllers
Check for:
- Authorization on all resource operations
-
before_actionwithonly:/except: - Strong parameters (no direct
params[]access) - Status
:unprocessable_contenton validation failures - Consistent flash messages:
Successfully [Action] [Resource] - Proper naming conventions
- State validations in before_actions (not in main actions)
Priority order: Security (authorization, params) → RESTful patterns → Status codes → Messaging
More from rolemodel/rolemodel-skills
bem-structure
Expert guidance for writing, refactoring, and structuring CSS using BEM (Block Element Modifier) methodology. Provides proper CSS class naming conventions, component structure, and Optics design system integration for maintainable, scalable stylesheets.
82optics-context
Use the Optics design framework for styling applications. Apply Optics classes for layout, spacing, typography, colors, and components. Use when working on CSS, styling views, or implementing design system guidelines.
37routing-patterns
Review, generate, and update Rails routes following professional patterns and best practices. Covers RESTful resource routing, route concerns for code reusability, shallow nesting strategies, and advanced route configurations.
28turbo-fetch
Implement dynamic form updates using Turbo Streams and Stimulus. Use when forms need to update fields based on user selections without full page reloads, such as cascading dropdowns, conditional fields, or dynamic option lists.
27stimulus-controllers
Create and register Stimulus controllers for interactive JavaScript features. Use when adding client-side interactivity, dynamic UI updates, or when the user mentions Stimulus controllers or JavaScript behavior.
26testing-patterns
Write automated tests using RSpec, Capybara, and FactoryBot for Rails applications. Use when implementing features, fixing bugs, or when the user mentions testing, specs, RSpec, Capybara, or test data. Avoid using rails console or server for testing.
26