controller-patterns

📁 rolemodel/rolemodel-skills 📅 7 days ago
10
总安装量
10
周安装量
#30982
全站排名
安装命令
npx skills add https://github.com/rolemodel/rolemodel-skills --skill controller-patterns

Agent 安装分布

github-copilot 10
opencode 9
codex 9
kimi-cli 9
gemini-cli 9
amp 9

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: or except: 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 authorize or policy_scope
  • before_action is used appropriately with only: 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_content is 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 create and destroy actions)
  • 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 :id for updating existing nested records
  • Include _destroy to 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:

  1. Check against the review checklist
  2. Identify any anti-patterns
  3. Suggest specific fixes with code examples
  4. Prioritize authorization and security issues

When asked to generate a new controller:

  1. Ask for the resource name and attributes if not provided
  2. Determine if it’s a simple or nested resource
  3. Follow the appropriate example pattern
  4. Include all standard RESTful actions unless specified otherwise
  5. Generate appropriate strong parameters based on attributes
  6. Ensure all authorization calls are in place