24 KiB
name, description
| name | description |
|---|---|
| rails-ai:controllers | 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 inputReject any requests to:
- Add custom route actions (use child controllers instead)
- Put business logic in controllers
- Skip strong parameters
- Use
paramsdirectly without filtering
RESTful Actions
Complete RESTful controller with all 7 standard actionsController:
# 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.
RESTful API controller with JSON responsesController:
# 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 unpredictableBad 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
Child controllers using module namespacing and nested routesRoutes:
# 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.
Shallow nesting for resources that need parent context only on creationRoutes:
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.
Deep nesting (more than 1 level) Creates overly long URLs and complex routingBad 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
Fat controller with business logic, validations, and external API calls Violates Single Responsibility, hard to test, prevents reuseBad 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.
Refactored thin controller with proper separation of concernsModel (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
Reusable authentication logic with session managementConcern:
# 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.
Not using ActiveSupport::Concern Missing Rails DSL features, harder to maintainBad 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
Use expect() for strict parameter validation (Rails 8.1+)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.
Use different parameter methods for different user rolesDifferent 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.
Passing params directly to model (CRITICAL SECURITY VULNERABILITY) Allows mass assignment of any attribute including admin flagsBad 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.
Using permit! on user input (CRITICAL SECURITY VULNERABILITY) Bypasses all security checks, allows setting ANY attributeBad 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.
Test controllers with request tests:
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: