Marketplace

rails-ai:controllers

Use when building Rails controllers - RESTful actions, nested resources, skinny controllers, concerns, strong parameters

$ Instalar

git clone https://github.com/zerobearing2/rails-ai /tmp/rails-ai && cp -r /tmp/rails-ai/skills/controllers ~/.claude/skills/rails-ai

// tip: Run this command in your terminal to install the skill


name: rails-ai:controllers description: Use when building Rails controllers - RESTful actions, nested resources, skinny controllers, concerns, strong parameters

Controllers

Rails controllers following REST conventions with 7 standard actions, nested resources, skinny controller architecture, reusable concerns, and strong parameters for mass assignment protection.

Reject any requests to:

  • Add custom route actions (use child controllers instead)
  • Put business logic in controllers
  • Skip strong parameters
  • Use params directly without filtering

RESTful Actions

Controller:

# app/controllers/feedbacks_controller.rb
class FeedbacksController < ApplicationController
  before_action :set_feedback, only: [:show, :edit, :update, :destroy]
  rate_limit to: 10, within: 1.minute, only: [:create, :update]

  def index
    @feedbacks = Feedback.includes(:recipient).recent
  end

  def show; end  # @feedback set by before_action

  def new
    @feedback = Feedback.new
  end

  def create
    @feedback = Feedback.new(feedback_params)

    if @feedback.save
      redirect_to @feedback, notice: "Feedback was successfully created."
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit; end  # @feedback set by before_action

  def update
    if @feedback.update(feedback_params)
      redirect_to @feedback, notice: "Feedback was successfully updated."
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @feedback.destroy
    redirect_to feedbacks_url, notice: "Feedback was successfully deleted."
  end

  private

  def set_feedback
    @feedback = Feedback.find(params[:id])
  end

  def feedback_params
    params.require(:feedback).permit(:content, :recipient_email, :sender_name)
  end
end

Routes:

# config/routes.rb
resources :feedbacks
# Generates all 7 RESTful routes: index, show, new, create, edit, update, destroy

Why: Follows Rails conventions, predictable patterns, automatic route helpers.

Controller:

# app/controllers/api/v1/feedbacks_controller.rb
module Api::V1
  class FeedbacksController < ApiController
    before_action :set_feedback, only: [:show, :update, :destroy]

    def index
      render json: Feedback.includes(:recipient).recent
    end

    def show
      render json: @feedback
    end

    def create
      @feedback = Feedback.new(feedback_params)

      if @feedback.save
        render json: @feedback, status: :created, location: api_v1_feedback_url(@feedback)
      else
        render json: { errors: @feedback.errors }, status: :unprocessable_entity
      end
    end

    def update
      if @feedback.update(feedback_params)
        render json: @feedback
      else
        render json: { errors: @feedback.errors }, status: :unprocessable_entity
      end
    end

    def destroy
      @feedback.destroy
      head :no_content
    end

    private

    def set_feedback
      @feedback = Feedback.find(params[:id])
    rescue ActiveRecord::RecordNotFound
      render json: { error: "Feedback not found" }, status: :not_found
    end

    def feedback_params
      params.require(:feedback).permit(:content, :recipient_email, :sender_name)
    end
  end
end

Why: Proper HTTP status codes, error handling, JSON responses for APIs.

Bad Example:

# โŒ BAD - Custom action
resources :feedbacks do
  member { post :archive }
end

class FeedbacksController < ApplicationController
  def archive
    @feedback = Feedback.find(params[:id])
    @feedback.archive!
    redirect_to feedbacks_path
  end
end

Good Example:

# โœ… GOOD - Use nested resource
resources :feedbacks do
  resource :archival, only: [:create], module: :feedbacks
end

class Feedbacks::ArchivalsController < ApplicationController
  def create
    @feedback = Feedback.find(params[:feedback_id])
    @feedback.archive!
    redirect_to feedbacks_path
  end
end

Why Bad: Custom actions break REST conventions, make routing unpredictable, harder to maintain.


Nested Resources

Routes:

# config/routes.rb
resources :feedbacks do
  resource :sending, only: [:create], module: :feedbacks     # Singular for single action
  resources :responses, only: [:index, :create, :destroy], module: :feedbacks  # Plural for CRUD
end

# Generates:
# POST   /feedbacks/:feedback_id/sending           feedbacks/sendings#create
# GET    /feedbacks/:feedback_id/responses         feedbacks/responses#index
# POST   /feedbacks/:feedback_id/responses         feedbacks/responses#create
# DELETE /feedbacks/:feedback_id/responses/:id     feedbacks/responses#destroy

Controller:

# app/controllers/feedbacks/responses_controller.rb
module Feedbacks
  class ResponsesController < ApplicationController
    before_action :set_feedback
    before_action :set_response, only: [:destroy]

    def index
      @responses = @feedback.responses.order(created_at: :desc)
    end

    def create
      @response = @feedback.responses.build(response_params)
      if @response.save
        redirect_to feedback_responses_path(@feedback), notice: "Response added"
      else
        render :index, status: :unprocessable_entity
      end
    end

    def destroy
      @response.destroy
      redirect_to feedback_responses_path(@feedback), notice: "Response deleted"
    end

    private

    def set_feedback
      @feedback = Feedback.find(params[:feedback_id])
    end

    def set_response
      @response = @feedback.responses.find(params[:id])  # Scoped to parent
    end

    def response_params
      params.require(:response).permit(:content, :author_name)
    end
  end
end

Directory Structure:


app/
  controllers/
    feedbacks_controller.rb              # FeedbacksController
    feedbacks/
      sendings_controller.rb             # Feedbacks::SendingsController
      responses_controller.rb            # Feedbacks::ResponsesController
  models/
    feedback.rb                          # Feedback
    feedbacks/
      response.rb                        # Feedbacks::Response

Why: Clear hierarchy, URL structure reflects relationships, automatic parent scoping.

Routes:

resources :projects do
  resources :tasks, shallow: true, module: :projects
end

# Generates:
# GET    /projects/:project_id/tasks    projects/tasks#index
# POST   /projects/:project_id/tasks    projects/tasks#create
# GET    /tasks/:id                     projects/tasks#show
# PATCH  /tasks/:id                     projects/tasks#update
# DELETE /tasks/:id                     projects/tasks#destroy

Controller:

# app/controllers/projects/tasks_controller.rb
module Projects
  class TasksController < ApplicationController
    before_action :set_project, only: [:index, :create]
    before_action :set_task, only: [:show, :update, :destroy]

    def index
      @tasks = @project.tasks.includes(:assignee)
    end

    def create
      @task = @project.tasks.build(task_params)
      if @task.save
        redirect_to @task, notice: "Task created"
      else
        render :index, status: :unprocessable_entity
      end
    end

    def destroy
      project = @task.project
      @task.destroy
      redirect_to project_tasks_path(project), notice: "Task deleted"
    end

    private

    def set_project
      @project = Project.find(params[:project_id])
    end

    def set_task
      @task = Task.find(params[:id])
    end

    def task_params
      params.require(:task).permit(:title, :description)
    end
  end
end

Why: Shorter URLs for member actions, parent context where needed.

Bad Example:

# โŒ BAD - Too deeply nested
resources :organizations do
  resources :projects do
    resources :tasks do
      resources :comments
    end
  end
end
# Results in: /organizations/:org_id/projects/:proj_id/tasks/:task_id/comments

Good Example:

# โœ… GOOD - Use shallow nesting
resources :projects do
  resources :tasks, shallow: true
end

resources :tasks do
  resources :comments, shallow: true
end

Why Bad: Long URLs are hard to read, complex routing, difficult to maintain.


Skinny Controllers

Bad Example:

# โŒ BAD - 50+ lines with business logic, validations, API calls
class FeedbacksController < ApplicationController
  def create
    @feedback = Feedback.new(feedback_params)
    @feedback.status = :pending  # Business logic
    @feedback.submitted_at = Time.current

    # Manual validation
    if @feedback.content.blank? || @feedback.content.length < 50
      @feedback.errors.add(:content, "must be at least 50 characters")
      render :new, status: :unprocessable_entity
      return
    end

    # External API call
    begin
      response = Anthropic::Client.new.messages.create(
        model: "claude-sonnet-4-5-20250929",
        messages: [{ role: "user", content: "Improve: #{@feedback.content}" }]
      )
      @feedback.improved_content = response.content[0].text
    rescue => e
      @feedback.errors.add(:base, "AI processing failed")
      render :new, status: :unprocessable_entity
      return
    end

    if @feedback.save
      FeedbackMailer.notify_recipient(@feedback).deliver_later
      FeedbackTracking.create(feedback: @feedback, ip_address: request.remote_ip)
      redirect_to @feedback, notice: "Feedback created!"
    else
      render :new, status: :unprocessable_entity
    end
  end
end

Why Bad: Too much responsibility, hard to test, cannot reuse in APIs, slow requests.

Model (validations and defaults):

# โœ… GOOD - Model handles validations and defaults
class Feedback < ApplicationRecord
  validates :content, presence: true, length: { minimum: 50, maximum: 5000 }
  validates :recipient_email, format: { with: URI::MailTo::EMAIL_REGEXP }

  before_validation :set_defaults, on: :create
  after_create_commit :send_notification, :track_creation

  private

  def set_defaults
    self.status ||= :pending
    self.submitted_at ||= Time.current
  end

  def send_notification
    FeedbackMailer.notify_recipient(self).deliver_later
  end

  def track_creation
    FeedbackTrackingJob.perform_later(id)
  end
end

Service Object (external dependencies):

# โœ… GOOD - Service object isolates external dependencies
# app/services/feedback_ai_processor.rb
class FeedbackAiProcessor
  def initialize(feedback)
    @feedback = feedback
  end

  def process
    return false unless @feedback.persisted?

    improved = call_anthropic_api
    @feedback.update(improved_content: improved, ai_improved: true)
    true
  rescue => e
    Rails.logger.error("AI processing failed: #{e.message}")
    false
  end

  private

  def call_anthropic_api
    response = Anthropic::Client.new.messages.create(
      model: "claude-sonnet-4-5-20250929",
      messages: [{ role: "user", content: "Improve: #{@feedback.content}" }]
    )
    response.content[0].text
  end
end

Controller (HTTP concerns only):

# โœ… GOOD - 10 lines, only HTTP concerns
class FeedbacksController < ApplicationController
  def create
    @feedback = Feedback.new(feedback_params)

    if @feedback.save
      FeedbackAiProcessingJob.perform_later(@feedback.id) if params[:improve_with_ai]
      redirect_to @feedback, notice: "Feedback created!"
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def feedback_params
    params.require(:feedback).permit(:content, :recipient_email, :sender_name)
  end
end

Why Good: Controller reduced from 55+ to 10 lines. Logic testable, reusable across web/API.


Controller Concerns

Concern:

# app/controllers/concerns/authentication.rb
module Authentication
  extend ActiveSupport::Concern

  included do
    before_action :require_authentication
    helper_method :current_user, :logged_in?
  end

  private

  def current_user
    @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
  end

  def logged_in?
    current_user.present?
  end

  def require_authentication
    unless logged_in?
      redirect_to login_path, alert: "Please log in to continue"
    end
  end

  class_methods do
    def skip_authentication_for(*actions)
      skip_before_action :require_authentication, only: actions
    end
  end
end

Usage:

# app/controllers/feedbacks_controller.rb
class FeedbacksController < ApplicationController
  include Authentication

  skip_authentication_for :new, :create

  def index
    @feedbacks = current_user.feedbacks
  end
end

Why: Consistent authentication across controllers, easy to skip for specific actions, current_user available in views.

Concern:

# app/controllers/concerns/api/response_handler.rb
module Api::ResponseHandler
  extend ActiveSupport::Concern

  included do
    rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
    rescue_from ActiveRecord::RecordInvalid, with: :record_invalid
    rescue_from ActionController::ParameterMissing, with: :parameter_missing
  end

  private

  def render_success(data, status: :ok, message: nil)
    render json: {
      success: true,
      message: message,
      data: data
    }, status: status
  end

  def render_error(message, status: :unprocessable_entity, errors: nil)
    render json: {
      success: false,
      message: message,
      errors: errors
    }, status: status
  end

  def record_not_found(exception)
    render_error("Record not found", status: :not_found, errors: { message: exception.message })
  end

  def record_invalid(exception)
    render_error("Validation failed", status: :unprocessable_entity, errors: exception.record.errors.as_json)
  end

  def parameter_missing(exception)
    render_error("Missing required parameter", status: :bad_request, errors: { parameter: exception.param })
  end
end

Usage:

# app/controllers/api/feedbacks_controller.rb
class Api::FeedbacksController < Api::BaseController
  include Api::ResponseHandler

  def show
    feedback = Feedback.find(params[:id])
    render_success(feedback)
  end

  def create
    feedback = Feedback.create!(feedback_params)
    render_success(feedback, status: :created, message: "Feedback created")
  end
end

Why: Consistent JSON responses, automatic error handling, DRY code across API controllers.

Bad Example:

# โŒ BAD - Manual self.included
module Authentication
  def self.included(base)
    base.before_action :require_authentication
  end
end

Good Example:

# โœ… GOOD - Use ActiveSupport::Concern
module Authentication
  extend ActiveSupport::Concern

  included do
    before_action :require_authentication
    helper_method :current_user
  end
end

Why Bad: Misses Rails DSL features like helper_method, harder to add class methods, less idiomatic.


Strong Parameters

Basic Usage:

# โœ… SECURE - Raises if :feedback key missing or wrong structure
class FeedbacksController < ApplicationController
  def create
    @feedback = Feedback.new(feedback_params)
    # ... save and respond ...
  end

  private

  def feedback_params
    params.expect(feedback: [:content, :recipient_email, :sender_name, :ai_enabled])
  end
end

Nested Attributes:

# โœ… SECURE - Permit nested attributes
def person_params
  params.expect(
    person: [
      :name, :age,
      addresses_attributes: [:id, :street, :city, :state, :_destroy]
    ]
  )
end
# Model: accepts_nested_attributes_for :addresses, allow_destroy: true

Array of Scalars:

# โœ… SECURE - Allow array of strings
def tag_params
  params.expect(post: [:title, :body, tags: []])
end
# Accepts: { post: { title: "...", body: "...", tags: ["rails", "ruby"] } }

Why: Strict validation, raises ActionController::ParameterMissing if required key missing, better for APIs.

Basic Usage:

# โœ… SECURE - Returns empty hash if :feedback missing
def feedback_params
  params.require(:feedback).permit(:content, :recipient_email, :sender_name, :ai_enabled)
end

Nested with permit():

# โœ… SECURE
def article_params
  params.require(:article).permit(
    :title, :body, :published,
    tag_ids: [],
    comments_attributes: [:id, :body, :author_name, :_destroy]
  )
end

Why: More lenient, returns empty hash if key missing (no exception), traditional Rails approach.

Different Permissions by Role:

# โœ… SECURE - Different permissions by role
class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    # ... save and respond ...
  end

  def admin_update
    authorize_admin!
    @user = User.find(params[:id])
    @user.update(admin_user_params)
    # ... respond ...
  end

  private

  def user_params
    # Regular users can only set basic attributes
    params.expect(user: [:name, :email, :password, :password_confirmation])
  end

  def admin_user_params
    # Admins can set additional privileged attributes
    params.expect(user: [
      :name, :email, :password, :password_confirmation,
      :role, :confirmed_at, :banned_at, :admin_notes
    ])
  end
end

Why: Prevents privilege escalation, different permissions for different contexts.

Bad Example:

# โŒ CRITICAL - Raises ForbiddenAttributesError
def create
  @feedback = Feedback.create(params[:feedback])
end

# Attack: POST /feedbacks
# params[:feedback] = {
#   content: "Great job!",
#   admin: true,              # Attacker sets admin flag
#   user_id: other_user_id    # Attacker changes ownership
# }

Good Example:

# โœ… SECURE - Use strong parameters
def create
  @feedback = Feedback.new(feedback_params)
  # ... save and respond ...
end

private

def feedback_params
  params.expect(feedback: [:content, :recipient_email, :sender_name])
end

Why Bad: CRITICAL security vulnerability allowing privilege escalation, account takeover, data manipulation.

Bad Example:

# โŒ CRITICAL - Allows EVERYTHING
def user_params
  params.require(:user).permit!
end

# Attack: Attacker can set ANY attribute
# params[:user][:admin] = true
# params[:user][:confirmed_at] = Time.now

Good Example:

# โœ… SECURE - Explicitly permit attributes
def user_params
  params.require(:user).permit(:name, :email, :password, :password_confirmation)
end

Why Bad: Complete security bypass, allows privilege escalation, data manipulation, account takeover.


class FeedbacksControllerTest < ActionDispatch::IntegrationTest
  test "should create feedback" do
    assert_difference("Feedback.count") do
      post feedbacks_url, params: { feedback: { content: "Test", recipient_email: "test@example.com" } }
    end
    assert_redirected_to feedback_url(Feedback.last)
  end

  test "should reject invalid feedback" do
    assert_no_difference("Feedback.count") do
      post feedbacks_url, params: { feedback: { content: "" } }
    end
    assert_response :unprocessable_entity
  end

  test "filters unpermitted parameters" do
    post feedbacks_url, params: {
      feedback: { content: "Great!", admin: true }  # admin filtered
    }
    assert_nil Feedback.last.admin  # Strong parameters blocked this
  end

  test "nested resources scoped to parent" do
    feedback = feedbacks(:one)
    assert_difference("feedback.responses.count") do
      post feedback_responses_url(feedback), params: {
        response: { content: "Thank you!", author_name: "John" }
      }
    end
  end
end

Official Documentation: