CtrlK
BlogDocsLog inGet started
Tessl Logo

controller-patterns

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.

70

Quality

63%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./skills/controller-patterns/SKILL.md
SKILL.md
Quality
Evals
Security

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)
  • 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

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!'<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 changesNamespaced 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 :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 → Use Nested Resource pattern
  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)

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

Repository
RoleModel/rolemodel-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.