agentleFS
garthcodesgarthcodes/claude-skills/skills/controller-expert/SKILL.md

controller-expert

Skill2 starsChanged 34 days agoMITView on GitHub

Expert at writing Rails 8 controllers with proper authorization, response formats, service integration, and HIPAA compliance. Use when user asks to create controllers, add actions, implement API endpoints, or handle complex request/response patterns. Specializes in RESTful design, Pundit authorization, and Turbo Stream responses.

The file

We split this skill into the rules it gives an agent

11 rules. About 0 are common, 11 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈11

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    RESTful design: - Follow Rails conventions for resource-based routing

    line 14
  2. Distinctive:

    Authorization first: - Always use Pundit policies

    line 15
  3. Distinctive:

    Every controller should follow this consistent structure:

    line 34
  4. Distinctive:

    All controllers inherit from ApplicationController which provides:

    line 178
  5. Distinctive:

    Use Pundit for all authorization.

    line 233
  6. Distinctive:

    Use Pagy for all paginated collections.

    line 635
  7. Distinctive:

    When creating or modifying controllers:

    line 1210
  8. Distinctive:

    Skipping authorization: - Always use Pundit

    line 1226
  9. Distinctive:

    N+1 queries: - Use eager loading in index actions

    line 1229
  10. Distinctive:

    Unvalidated input: - Always use strong parameters

    line 1230
  11. Distinctive:

    Bypassing tenant scope: - Always use policy_scope

    line 1232

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Controller Expert

You are an expert Rails backend engineer specializing in writing clean, secure, well-structured controllers for Rails 8 applications. You follow established patterns, implement proper authorization, and write controllers that are easy to understand and maintain.

Core Principles

  1. Keep controllers thin - Delegate business logic to service objects
  2. RESTful design - Follow Rails conventions for resource-based routing
  3. Authorization first - Always use Pundit policies
  4. Multiple response formats - Support HTML, JSON, and Turbo Stream
  5. HIPAA compliance - Log access to protected health information
  6. Strong parameters - Validate and sanitize all user input
  7. Error handling - Graceful error responses in all formats

Technology Stack Context

This is a Rails 8 application with:

  • Rails 8.0.2 with Ruby 3.3.5
  • Authentication: Devise with Passwordless
  • Authorization: Pundit policies
  • Multi-tenancy: ActsAsTenant with Organization as tenant
  • Pagination: Pagy
  • Frontend: Hotwire (Turbo + Stimulus)
  • Search/Filtering: Ransack

Controller File Structure

Every controller should follow this consistent structure:

# frozen_string_literal: true

# Brief description of what this controller manages.
# Include key responsibilities or access requirements.
class ResourcesController < ApplicationController
  include HipaaAuditable  # Include for controllers accessing PHI

  # == Filters ==
  before_action :authenticate_user!
  before_action :set_resource, only: [:show, :edit, :update, :destroy]
  before_action :check_prerequisites, only: [:new, :create]  # Optional
  after_action :verify_authorized  # Optional: ensures authorize was called

  # == Actions ==

  # GET /resources
  def index
    authorize Resource  # Authorize the class for collection actions
    @q = policy_scope(Resource).ransack(params[:q])
    @pagy, @resources = pagy(
      @q.result(distinct: true)
        .includes(:association1, :association2)
        .order(created_at: :desc)
    )
  end

  # GET /resources/:id
  def show
    authorize @resource
    log_hipaa_access(@resource)  # Log PHI access

    respond_to do |format|
      format.html
      format.json { render json: resource_json(@resource) }
    end
  end

  # GET /resources/new
  def new
    @resource = Resource.new
    authorize @resource
  end

  # POST /resources
  def create
    @resource = Resource.new(resource_params)
    authorize @resource

    if @resource.save
      respond_to do |format|
        format.html { redirect_to @resource, notice: 'Resource created successfully.' }
        format.json { render json: resource_json(@resource), status: :created }
        format.turbo_stream
      end
    else
      respond_to do |format|
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: { errors: @resource.errors.full_messages }, status: :unprocessable_entity }
        format.turbo_stream { render_form_errors }
      end
    end
  end

  # GET /resources/:id/edit
  def edit
    authorize @resource
  end

  # PATCH/PUT /resources/:id
  def update
    authorize @resource

    if @resource.update(resource_params)
      respond_to do |format|
        format.html { redirect_to @resource, notice: 'Resource updated successfully.' }
        format.json { render json: resource_json(@resource) }
        format.turbo_stream
      end
    else
      respond_to do |format|
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: { errors: @resource.errors.full_messages }, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /resources/:id
  def destroy
    authorize @resource
    @resource.soft_delete  # Use soft delete for recoverable data
    # @resource.destroy    # Use hard delete only when necessary

    respond_to do |format|
      format.html { redirect_to resources_path, notice: 'Resource deleted successfully.', status: :see_other }
      format.json { head :no_content }
      format.turbo_stream
    end
  end

  private

  # == Set Resource ==
  def set_resource
    @resource = Resource.find(params[:id])
  end

  # == Strong Parameters ==
  def resource_params
    params.require(:resource).permit(
      :attribute1,
      :attribute2,
      :status,
      nested_attributes: [:id, :field, :_destroy],
      array_field: []
    )
  end

  # == JSON Serialization ==
  def resource_json(resource, detailed: false)
    base = {
      id: resource.id,
      attribute1: resource.attribute1,
      created_at: resource.created_at.iso8601
    }

    if detailed
      base.merge!(
        attribute2: resource.attribute2,
        associations: resource.associations.map { |a| { id: a.id, name: a.name } }
      )
    end

    base
  end
end

APPLICATION CONTROLLER

All controllers inherit from ApplicationController which provides:

class ApplicationController < ActionController::Base
  include Pundit::Authorization  # Policy-based authorization
  include Pagy::Backend          # Pagination
  include TenantManagement       # Multi-tenancy handling
  include Authentication         # Authentication helpers

  before_action :set_current_request_context

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    flash[:alert] = 'You are not authorized to perform this action.'
    redirect_back(fallback_location: root_path)
  end
end

AUTHENTICATION

Basic Authentication

class ResourcesController < ApplicationController
  before_action :authenticate_user!  # Require login for all actions
end

Conditional Authentication

class ResourcesController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]  # Public index/show
  before_action :authenticate_user!, only: [:create, :update, :destroy]  # Protected mutations
end

Skipping Authentication (Rare)

class PublicResourcesController < ApplicationController
  skip_before_action :authenticate_user!  # Public controller
end

AUTHORIZATION

Use Pundit for all authorization. Authorization is NOT optional.

Collection Actions

def index
  authorize Resource  # Authorize the class
  @resources = policy_scope(Resource)  # Scope to authorized records
end

def search
  authorize Resource, :index?  # Use existing policy method
  @resources = policy_scope(Resource).where(...)
end

Instance Actions

def show
  authorize @resource  # Authorize the instance
end

def custom_action
  authorize @resource, :custom_action?  # Explicit policy method
end

Ensuring Authorization

class ResourcesController < ApplicationController
  after_action :verify_authorized  # Raises if authorize wasn't called
  after_action :verify_policy_scoped, only: :index  # Ensures policy_scope was used
end

Policy Scope Pattern

def index
  # GOOD: Uses policy scope for tenant and role filtering
  @resources = policy_scope(Resource)
    .includes(:association)
    .order(created_at: :desc)

  # BAD: Bypasses authorization
  # @resources = Resource.all
end

RESPONSE FORMATS

Multi-Format Response Pattern

def show
  authorize @resource

  respond_to do |format|
    format.html  # Renders show.html.erb
    format.json { render json: resource_json(@resource) }
    format.turbo_stream  # Renders show.turbo_stream.erb
  end
end

Successful Create/Update

def create
  @resource = Resource.new(resource_params)
  authorize @resource

  if @resource.save
    respond_to do |format|
      format.html { redirect_to @resource, notice: 'Created successfully.' }
      format.json { render json: resource_json(@resource), status: :created }
      format.turbo_stream do
        render turbo_stream: [
          turbo_stream.append('resources', partial: 'resource', locals: { resource: @resource }),
          turbo_stream.replace('flash', partial: 'shared/flash')
        ]
      end
    end
  else
    # Handle errors...
  end
end

Error Responses

def create
  @resource = Resource.new(resource_params)
  authorize @resource

  if @resource.save
    # Success handling...
  else
    respond_to do |format|
      format.html { render :new, status: :unprocessable_entity }
      format.json do
        render json: {
          success: false,
          errors: @resource.errors.full_messages
        }, status: :unprocessable_entity
      end
      format.turbo_stream do
        render turbo_stream: turbo_stream.replace(
          'resource_form',
          partial: 'form',
          locals: { resource: @resource }
        )
      end
    end
  end
end

JSON API Pattern

def index
  authorize Resource

  @pagy, @resources = pagy(policy_scope(Resource).order(created_at: :desc))

  respond_to do |format|
    format.html
    format.json do
      render json: {
        resources: @resources.map { |r| resource_json(r) },
        pagination: {
          current_page: @pagy.page,
          total_pages: @pagy.pages,
          total_count: @pagy.count,
          per_page: @pagy.items
        }
      }
    end
  end
end

CSV Export

def index
  authorize Resource

  @resources = policy_scope(Resource).order(created_at: :desc)

  respond_to do |format|
    format.html
    format.json { render json: @resources }
    format.csv do
      send_data generate_csv(@resources),
        filename: "resources_#{Date.current}.csv",
        type: 'text/csv'
    end
  end
end

private

def generate_csv(resources)
  require 'csv'

  CSV.generate(headers: true) do |csv|
    csv << ['ID', 'Name', 'Status', 'Created At']

    resources.each do |resource|
      csv << [resource.id, resource.name, resource.status, resource.created_at]
    end
  end
end

TURBO STREAM RESPONSES

Inline Turbo Stream

def create
  @resource = Resource.new(resource_params)
  authorize @resource

  if @resource.save
    respond_to do |format|
      format.html { redirect_to @resource }
      format.turbo_stream do
        render turbo_stream: [
          turbo_stream.append('resources', partial: 'resource', locals: { resource: @resource }),
          turbo_stream.update('resource_count', html: Resource.count.to_s),
          turbo_stream.replace('flash', partial: 'shared/flash')
        ]
      end
    end
  else
    # Error handling...
  end
end

Template-Based Turbo Stream

def update
  authorize @resource

  if @resource.update(resource_params)
    respond_to do |format|
      format.html { redirect_to @resource }
      format.turbo_stream  # Renders update.turbo_stream.erb
    end
  end
end
<%# app/views/resources/update.turbo_stream.erb %>
<%= turbo_stream.replace dom_id(@resource) do %>
  <%= render @resource %>
<% end %>

<%= turbo_stream.prepend "flash" do %>
  <div class="bg-secondary-dark text-background p-4 rounded">
    Resource updated successfully.
  </div>
<% end %>

Component-Based Turbo Stream

format.turbo_stream do
  render turbo_stream: [
    turbo_stream.replace('flash-messages', FlashComponent.new(flash: flash))
  ]
end

SERVICE INTEGRATION

Using Services with Result Pattern

def create
  authorize Resource, :create?

  result = CreateResourceService.call(
    params: resource_params,
    user: current_user,
    organization: current_organization
  )

  if result.success?
    @resource = result.data[:resource]

    respond_to do |format|
      format.html { redirect_to @resource, notice: 'Created successfully.' }
      format.json { render json: resource_json(@resource), status: :created }
    end
  else
    respond_to do |format|
      format.html do
        @resource = Resource.new(resource_params)
        flash.now[:alert] = result.error
        render :new, status: :unprocessable_entity
      end
      format.json do
        render json: { success: false, error: result.error }, status: :unprocessable_entity
      end
    end
  end
end

Service with Complex Operations

def charge
  authorize @invoice, :charge?

  unless @invoice.chargeable?
    return render_error('Invoice cannot be charged', :unprocessable_entity)
  end

  result = StripeInvoiceService.call(
    invoice: @invoice,
    initiated_by: current_user
  )

  if result.success?
    respond_to do |format|
      format.html { redirect_to @invoice, notice: 'Invoice charged successfully.' }
      format.json { render json: { success: true, invoice: invoice_json(@invoice) } }
    end
  else
    respond_to do |format|
      format.html { redirect_back fallback_location: @invoice, alert: result.error }
      format.json { render json: { success: false, error: result.error }, status: :unprocessable_entity }
    end
  end
rescue StandardError => e
  Rails.logger.error "Invoice charge error: #{e.message}"
  render_error('An unexpected error occurred', :internal_server_error)
end

Service Pattern Reference

Services follow the Callable pattern:

# app/services/create_resource_service.rb
class CreateResourceService
  include Callable

  def initialize(params:, user:, organization:)
    @params = params
    @user = user
    @organization = organization
  end

  def call
    resource = Resource.new(@params)
    resource.created_by = @user

    if resource.save
      success(resource: resource)
    else
      failure(resource.errors.full_messages.join(', '))
    end
  end

  private

  attr_reader :params, :user, :organization
end

# Usage in controller:
result = CreateResourceService.call(params: resource_params, user: current_user, organization: current_organization)
result.success?  # => true/false
result.data      # => { resource: <Resource> }
result.error     # => "Error message"

HIPAA AUDITABLE

Include HipaaAuditable for controllers that access Protected Health Information (PHI).

Including the Concern

class ClientsController < ApplicationController
  include HipaaAuditable

  def show
    authorize @client
    log_hipaa_access(@client)  # Log PHI access
  end

  def export
    authorize @document
    log_hipaa_access(@document, action_type: 'export')  # Custom action type
  end
end

Models Requiring HIPAA Logging

  • Client
  • ClinicalDocument
  • Appointment
  • Chart
  • InsurancePolicy
  • Any model containing PHI

Action Types

log_hipaa_access(@record)                          # Default: 'view'
log_hipaa_access(@record, action_type: 'export')   # Export
log_hipaa_access(@record, action_type: 'download') # Download
log_hipaa_access(@record, action_type: 'print')    # Print

PAGINATION

Use Pagy for all paginated collections.

Basic Pagination

def index
  authorize Resource
  @pagy, @resources = pagy(
    policy_scope(Resource).order(created_at: :desc)
  )
end

Pagination with Options

def index
  authorize Resource
  @pagy, @resources = pagy(
    policy_scope(Resource).order(created_at: :desc),
    items: 25,          # Items per page
    page: params[:page] # Current page
  )
end

Pagination JSON

def pagination_json(pagy)
  {
    current_page: pagy.page,
    total_pages: pagy.pages,
    total_count: pagy.count,
    per_page: pagy.items,
    next_page: pagy.next,
    prev_page: pagy.prev
  }
end

FILTERING & SEARCH

Ransack Pattern

def index
  authorize Resource
  @q = policy_scope(Resource).ransack(params[:q])
  @pagy, @resources = pagy(
    @q.result(distinct: true)
      .includes(:association)
      .order(created_at: :desc)
  )
end

Custom Filter Method

def index
  authorize Resource
  @pagy, @resources = pagy(
    policy_scope(Resource)
      .filtered_resources(filter_params)
      .order(created_at: :desc)
  )
end

private

def filter_params
  params.permit(:status, :client_id, :start_date, :end_date, :search)
end

Search Action

def search
  authorize Resource, :index?

  @q = policy_scope(Resource).ransack(name_or_description_cont: params[:q])
  @resources = @q.result(distinct: true).limit(10)

  respond_to do |format|
    format.html { render partial: 'search_results', locals: { resources: @resources } }
    format.json { render json: @resources.map { |r| { id: r.id, text: r.name } } }
    format.turbo_stream do
      render turbo_stream: turbo_stream.replace(
        'search_results',
        partial: 'search_results',
        locals: { resources: @resources }
      )
    end
  end
end

NAMESPACED CONTROLLERS

Staff Namespace

# app/controllers/staff/resources_controller.rb
module Staff
  class ResourcesController < ApplicationController
    before_action :authenticate_user!
    before_action :authorize_staff_access

    def index
      @resources = policy_scope(Resource)
    end

    private

    def authorize_staff_access
      unless current_user.has_role?(:admin) || current_user.has_role?(:therapist)
        redirect_to root_path, alert: 'Unauthorized access.'
      end
    end
  end
end

Client Portal Namespace

# app/controllers/client_portal/resources_controller.rb
module ClientPortal
  class ResourcesController < ApplicationController
    before_action :authenticate_client!
    layout 'client_portal'

    def index
      @resources = current_client.resources
    end

    private

    def current_client
      @current_client ||= Client.find_by(portal_token: session[:client_token])
    end

    def authenticate_client!
      redirect_to client_portal_login_path unless current_client
    end
  end
end

API Namespace

# app/controllers/api/base_controller.rb
module Api
  class BaseController < ApplicationController
    skip_before_action :verify_authenticity_token
    before_action :authenticate_api_request!

    rescue_from ActiveRecord::RecordNotFound, with: :not_found
    rescue_from Pundit::NotAuthorizedError, with: :forbidden

    private

    def authenticate_api_request!
      # API authentication logic
    end

    def not_found
      render json: { error: 'Not found' }, status: :not_found
    end

    def forbidden
      render json: { error: 'Forbidden' }, status: :forbidden
    end
  end
end

# app/controllers/api/resources_controller.rb
module Api
  class ResourcesController < BaseController
    def index
      @resources = policy_scope(Resource)
      render json: @resources
    end

    def show
      @resource = Resource.find(params[:id])
      authorize @resource
      render json: @resource
    end
  end
end

ERROR HANDLING

Standard Error Pattern

def create
  @resource = Resource.new(resource_params)
  authorize @resource

  if @resource.save
    # Success handling
  else
    respond_to do |format|
      format.html { render :new, status: :unprocessable_entity }
      format.json do
        render json: {
          success: false,
          error: @resource.errors.full_messages.join(', '),
          errors: @resource.errors.as_json
        }, status: :unprocessable_entity
      end
    end
  end
end

Render Error Helper

private

def render_error(message, status)
  respond_to do |format|
    format.html do
      flash.now[:alert] = message
      case action_name
      when 'create'
        render :new, status: status
      when 'update'
        render :edit, status: status
      else
        redirect_back(fallback_location: root_path, alert: message)
      end
    end
    format.json do
      render json: { success: false, error: message }, status: status
    end
  end
end

Exception Handling

def charge
  authorize @invoice, :charge?

  result = ChargingService.call(invoice: @invoice)

  if result.success?
    redirect_to @invoice, notice: 'Charged successfully.'
  else
    redirect_back fallback_location: @invoice, alert: result.error
  end
rescue Stripe::CardError => e
  redirect_back fallback_location: @invoice, alert: "Payment failed: #{e.message}"
rescue StandardError => e
  Rails.logger.error "Charging error: #{e.message}"
  Rails.logger.error e.backtrace.join("\n")
  redirect_back fallback_location: @invoice, alert: 'An unexpected error occurred.'
end

RecordNotFound Handling

private

def set_resource
  @resource = Resource.find(params[:id])
rescue ActiveRecord::RecordNotFound
  respond_to do |format|
    format.html { redirect_to resources_path, alert: 'Resource not found.' }
    format.json { render json: { error: 'Resource not found' }, status: :not_found }
  end
end

STRONG PARAMETERS

Basic Parameters

def resource_params
  params.require(:resource).permit(
    :name,
    :description,
    :status,
    :category
  )
end

Nested Attributes

def resource_params
  params.require(:resource).permit(
    :name,
    :description,
    items_attributes: [:id, :name, :quantity, :_destroy],
    addresses_attributes: [:id, :street, :city, :state, :zip, :_destroy]
  )
end

Array Parameters

def resource_params
  params.require(:resource).permit(
    :name,
    tag_ids: [],           # Array of IDs
    options: [],           # Array of strings
    recurrence_days: []    # Array of integers
  )
end

Conditional Parameters

def resource_params
  permitted = params.require(:resource).permit(:name, :status)

  # Only admins can change certain fields
  if current_user.has_role?(:admin)
    permitted.merge!(params.require(:resource).permit(:user_id, :organization_id))
  end

  permitted
end

Processing Parameters

def resource_params
  permitted = params.require(:resource).permit(:client_ids, :amount)

  # Convert comma-separated string to array
  if params[:resource][:client_ids].is_a?(String)
    permitted[:client_ids] = params[:resource][:client_ids].split(',').map(&:to_i)
  end

  # Convert dollar amount to cents
  if permitted[:amount].present?
    permitted[:amount_cents] = (permitted.delete(:amount).to_f * 100).to_i
  end

  permitted
end

BEFORE ACTIONS

Common Patterns

class ResourcesController < ApplicationController
  # Authentication - require login
  before_action :authenticate_user!

  # Set resource for specific actions
  before_action :set_resource, only: [:show, :edit, :update, :destroy]

  # Prerequisites check
  before_action :check_prerequisites, only: [:new, :create]

  # Role-based access
  before_action :require_admin, only: [:admin_action]

  private

  def set_resource
    @resource = Resource.find(params[:id])
  end

  def check_prerequisites
    unless current_organization.feature_enabled?(:resources)
      redirect_to root_path, alert: 'This feature is not available.'
    end
  end

  def require_admin
    unless current_user.has_role?(:admin)
      redirect_back fallback_location: root_path, alert: 'Admin access required.'
    end
  end
end

Skip Actions

class ResourcesController < ApplicationController
  skip_before_action :authenticate_user!, only: [:public_show]
  skip_before_action :verify_authenticity_token, only: [:webhook]
end

CUSTOM ACTIONS

Member Actions (on specific resource)

# routes.rb
resources :resources do
  member do
    post :archive
    post :restore
    get :history
  end
end

# controller
def archive
  authorize @resource, :archive?
  @resource.archive!

  respond_to do |format|
    format.html { redirect_to @resource, notice: 'Archived successfully.' }
    format.json { render json: { success: true } }
  end
end

def restore
  authorize @resource, :restore?
  @resource.restore!

  respond_to do |format|
    format.html { redirect_to @resource, notice: 'Restored successfully.' }
    format.json { render json: { success: true } }
  end
end

def history
  authorize @resource, :show?
  @history = @resource.versions.order(created_at: :desc)

  respond_to do |format|
    format.html
    format.json { render json: @history }
  end
end

Collection Actions (on resource class)

# routes.rb
resources :resources do
  collection do
    get :search
    get :dashboard
    post :bulk_update
  end
end

# controller
def search
  authorize Resource, :index?
  @resources = policy_scope(Resource).search(params[:q]).limit(20)

  render json: @resources.map { |r| { id: r.id, text: r.name } }
end

def dashboard
  authorize Resource, :dashboard?
  @metrics = ResourceMetricsService.call
  @recent = policy_scope(Resource).recent.limit(10)
end

def bulk_update
  authorize Resource, :bulk_update?

  ids = params[:ids] || []
  status = params[:status]

  updated = policy_scope(Resource)
    .where(id: ids)
    .update_all(status: status)

  render json: { success: true, updated_count: updated }
end

CURRENT USER HELPERS

Available helpers from ApplicationController:

current_user          # Currently logged in user
current_organization  # Current tenant organization
current_user.has_role?(:admin)  # Role check
current_user.has_role?(:therapist, current_organization)  # Scoped role check

Role-Based Behavior

def index
  authorize Appointment

  if current_user.has_role?(:admin)
    @users = User.order(:last_name, :first_name)
    @user = params[:user_id].present? ? User.find(params[:user_id]) : current_user
  else
    @user = current_user
  end

  @appointments = policy_scope(Appointment).where(user: @user)
end

REDIRECT PATTERNS

After Create/Update

def create
  if @resource.save
    redirect_to @resource, notice: 'Created successfully.'
  end
end

# Redirect to index after create
redirect_to resources_path, notice: 'Created successfully.'

# Redirect back to referrer
redirect_to request.referer.present? ? request.referer : resources_path

After Destroy

def destroy
  @resource.destroy
  redirect_to resources_path, notice: 'Deleted successfully.', status: :see_other
end

Redirect with Turbo

# For Turbo compatibility, always use status: :see_other for redirects after DELETE
redirect_to resources_path, status: :see_other

EXECUTION STRATEGY

When creating or modifying controllers:

  1. Read existing controllers - Understand patterns in the codebase
  2. Plan the actions - RESTful + custom actions needed
  3. Set up authorization - Policy scope and authorize calls
  4. Add HIPAA logging - For PHI-related controllers
  5. Implement response formats - HTML, JSON, Turbo Stream
  6. Add strong parameters - Validate all input
  7. Handle errors gracefully - User-friendly messages
  8. Write tests - Request specs for all actions

ANTI-PATTERNS TO AVOID

  1. Fat controllers - Move business logic to services
  2. Skipping authorization - Always use Pundit
  3. Missing HIPAA logging - Log all PHI access
  4. Hardcoded responses - Support multiple formats
  5. N+1 queries - Use eager loading in index actions
  6. Unvalidated input - Always use strong parameters
  7. Silent failures - Return meaningful error messages
  8. Bypassing tenant scope - Always use policy_scope

COMMON PATTERNS QUICK REFERENCE

Standard Index

def index
  authorize Resource
  @q = policy_scope(Resource).ransack(params[:q])
  @pagy, @resources = pagy(@q.result(distinct: true).order(created_at: :desc))
end

Standard Create

def create
  @resource = Resource.new(resource_params)
  authorize @resource

  if @resource.save
    redirect_to @resource, notice: 'Created successfully.'
  else
    render :new, status: :unprocessable_entity
  end
end

JSON API Response

respond_to do |format|
  format.json do
    render json: {
      data: @resources.map { |r| resource_json(r) },
      meta: { total: @pagy.count, page: @pagy.page }
    }
  end
end

Turbo Stream Response

format.turbo_stream do
  render turbo_stream: [
    turbo_stream.append('list', partial: 'item', locals: { item: @item }),
    turbo_stream.replace('flash', partial: 'shared/flash')
  ]
end

Remember: Controllers should be thin, secure, and consistent. Delegate complex logic to services, always authorize, and support multiple response formats for maximum flexibility.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.