mcpField
C
controller-patterns

controller-patterns

github.com/rolemodelDevelopment743 installsUpdated 1d ago
Repository

About controller-patterns

A skill for generating and reviewing Ruby on Rails controllers, aimed at developers building RESTful application endpoints. It provides patterns for authorization, nested resources, state transitions, bulk operations, parameters, and response handling.

Key features

  1. 01RESTful CRUD controller patterns
  2. 02Pundit authorization guidance
  3. 03Nested and state-transition controllers
  4. 04Bulk operation patterns
  5. 05Strong parameters and status codes

Use cases

  1. 01Creating a controller for standard CRUD actions
  2. 02Adding authorization to resource operations
  3. 03Modeling approval or activation transitions
  4. 04Reviewing nested resource and bulk controllers

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/rolemodel/rolemodel-skills --skill controller-patterns

After installing, type/controller-patternsin Claude Code to confirm it loaded.

Details

From the project README

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 a single parent resource?
│  └─ Use: Nested RESTful Controller Pattern
│
├─ Child resource that hangs off several different parents?
│  (comments, reports, duplications, attachments)
│  └─ Use: polymorphic-parent-resources skill
│     (one route concern + one controller, not one controller per parent)
│
└─ 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)
  • authorize in set_* 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: or except:
  • 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 :id for update, _destroy for 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

Use this when the child has exactly one kind of parent. If the child can hang off several different parents (comments, reports, duplications, attachments), do not copy this per parent — read the polymorphic-parent-resources skill instead.

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_entityrender :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 changesNamespaced controller with RESTful actions
A namespaced controller per parent (Estimates::CommentsController, Widgets::CommentsController)One controller for every parent — see the polymorphic-parent-resources skill
add_comment / create_report action on the parent controllerSeparate RESTful child controller — see the polymorphic-parent-resources skill

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 :id for updating existing nested records
  • Include _destroy for 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
  1. Identify controller type:

    • Standard CRUD → Use Simple CRUD pattern
    • State transitions → Use Namespaced State pattern
    • Bulk operations → Use Bulk Operation pattern
    • Nested resource under one parent → Use Nested Resource pattern
    • Child resource with several possible parents → Read the polymorphic-parent-resources skill
  2. 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
  3. Follow conventions:

    • Name: ResourcesController or Resources::StatesController
    • Inherit from: ApplicationController
    • Instance variables: @resource (singular), @resources (plural)
    • Private methods: set_resource, resource_params
Reviewing Existing Controllers

Check for:

  • Authorization on all resource operations
  • before_action with only:/except:
  • Strong parameters (no direct params[] access)
  • Status :unprocessable_content on validation failures
  • Consistent flash messages: Successfully [Action] [Resource]
  • Proper naming conventions
  • State validations in before_actions (not in main actions)
  • No sibling controllers that differ only by which parent they load (see polymorphic-parent-resources)

Priority order: Security (authorization, params) → RESTful patterns → Status codes → Messaging

Comments

Markdown supported