--- 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. - Building Rails controller actions - Implementing nested resources - Handling request parameters - Setting up routing - Refactoring fat controllers - Sharing behavior with concerns - Protecting from mass assignment - **RESTful Conventions** - Predictable URL patterns and HTTP semantics - **Clean Architecture** - Skinny controllers with logic in appropriate layers - **Secure by Default** - Strong parameters prevent mass assignment - **Reusable Patterns** - Concerns share behavior across controllers - **Maintainable** - Clear separation of HTTP concerns from business logic **This skill enforces:** - ✅ **Rule #3:** NEVER add custom route actions → RESTful resources only - ✅ **Rule #7:** Thin controllers (delegate to models/services) - ✅ **Rule #10:** Strong parameters for all user input **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 Before completing controller work: - ✅ Only RESTful actions used (index, show, new, create, edit, update, destroy) - ✅ Child controllers created for non-REST actions (not custom actions) - ✅ Controllers are thin (<100 lines) - ✅ Strong parameters used for all user input - ✅ Business logic delegated to models/services - ✅ All controller actions tested - ✅ All tests passing - Use only 7 standard actions: index, show, new, create, edit, update, destroy - NO custom actions - use nested resources or services instead (TEAM RULE #3) - Keep controllers under 50 lines, actions under 10 lines - Move business logic to models or service objects - Always use strong parameters with expect() or require().permit() - Use before_action for common setup, not business logic - Return proper HTTP status codes (200, 201, 422, 404) --- ## RESTful Actions Complete RESTful controller with all 7 standard actions **Controller:** ```ruby # 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:** ```ruby # 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. RESTful API controller with JSON responses **Controller:** ```ruby # 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. Adding custom actions instead of using nested resources Breaks REST conventions and makes routing unpredictable **Bad Example:** ```ruby # ❌ 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:** ```ruby # ✅ 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 Child controllers using module namespacing and nested routes **Routes:** ```ruby # 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:** ```ruby # 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. Shallow nesting for resources that need parent context only on creation **Routes:** ```ruby 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:** ```ruby # 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. Deep nesting (more than 1 level) Creates overly long URLs and complex routing **Bad Example:** ```ruby # ❌ 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:** ```ruby # ✅ 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 Fat controller with business logic, validations, and external API calls Violates Single Responsibility, hard to test, prevents reuse **Bad Example:** ```ruby # ❌ 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. Refactored thin controller with proper separation of concerns **Model (validations and defaults):** ```ruby # ✅ 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):** ```ruby # ✅ 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):** ```ruby # ✅ 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 Reusable authentication logic with session management **Concern:** ```ruby # 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:** ```ruby # 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. Standardized JSON responses and error handling for APIs **Concern:** ```ruby # 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:** ```ruby # 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. Not using ActiveSupport::Concern Missing Rails DSL features, harder to maintain **Bad Example:** ```ruby # ❌ BAD - Manual self.included module Authentication def self.included(base) base.before_action :require_authentication end end ``` **Good Example:** ```ruby # ✅ 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 Use expect() for strict parameter validation (Rails 8.1+) **Basic Usage:** ```ruby # ✅ 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:** ```ruby # ✅ 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:** ```ruby # ✅ 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. Use require().permit() for more lenient validation **Basic Usage:** ```ruby # ✅ 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():** ```ruby # ✅ 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. Use different parameter methods for different user roles **Different Permissions by Role:** ```ruby # ✅ 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. Passing params directly to model (CRITICAL SECURITY VULNERABILITY) Allows mass assignment of any attribute including admin flags **Bad Example:** ```ruby # ❌ 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:** ```ruby # ✅ 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. Using permit! on user input (CRITICAL SECURITY VULNERABILITY) Bypasses all security checks, allows setting ANY attribute **Bad Example:** ```ruby # ❌ 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:** ```ruby # ✅ 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. --- Test controllers with request tests: ```ruby 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 ``` - rails-ai:models - Model validations, callbacks, associations - rails-ai:views - Forms, Turbo Frames/Streams - rails-ai:security - XSS, CSRF, SQL injection prevention - rails-ai:testing - Controller and integration testing **Official Documentation:** - [Rails Guides - Action Controller Overview](https://guides.rubyonrails.org/action_controller_overview.html) - [Rails Guides - Routing](https://guides.rubyonrails.org/routing.html) - [Rails Guides - Strong Parameters](https://guides.rubyonrails.org/action_controller_overview.html#strong-parameters) - [Rails API - ActiveSupport::Concern](https://api.rubyonrails.org/classes/ActiveSupport/Concern.html) - [Rails Edge Guides - Parameters expect()](https://edgeguides.rubyonrails.org/action_controller_overview.html#parameters-expect) - Rails 8.1+