| name | controller |
| description | Create a controller with commands, read model queries, routes, and integration tests |
Controller Builder
When to use
Use this skill when asked to create a new controller or add actions to an existing controller in any Rails app under apps/.
Working Directory
Determine which app the controller belongs to. Default is apps/rails_application/ unless specified.
Step-by-step process
1. Gather requirements
Before writing any code, clarify:
- Which read model the controller queries (must already exist)
- Which domain commands the controller dispatches (must already exist)
- What actions are needed (index, show, new, create, edit, update, destroy, custom)
- Whether it lives in a namespace (e.g.
admin/, client/)
2. Write integration tests first (TDD)
Create a test file at test/integration/{controller_name}_test.rb.
require "test_helper"
class ResourceNameTest < InMemoryRESIntegrationTestCase
def test_list_resources
get "/resources"
assert_response :success
end
def test_create_resource
post "/resources", params: { name: "Test" }
follow_redirect!
assert_select("td", "Test")
end
def test_update_resource
resource_id = create_resource("Original")
patch "/resources/#{resource_id}", params: { name: "Updated" }
follow_redirect!
assert_select("td", "Updated")
end
private
def create_resource(name)
post "/resources", params: { name: name }
follow_redirect!
ReadModel.all.last.uid
end
end
Integration test conventions:
- Inherit from
InMemoryRESIntegrationTestCase
- Use HTTP verbs:
get, post, patch, delete
- Use
follow_redirect! after redirecting actions
- Assert with
assert_response, assert_select
- Extract setup into private helper methods
- Never dispatch commands directly — always go through HTTP
- Never access AR models directly — use read model facade methods if needed
3. Add routes
Add to config/routes.rb:
resources :resources, only: [:index, :show, :new, :create, :edit, :update, :destroy] do
member do
post :custom_action
end
collection do
delete :clear_all
end
end
Route conventions:
4. Create the controller
class ResourcesController < ApplicationController
def index
@resources = ReadModel.all
end
def show
@resource = ReadModel.find_by_uid(params[:id])
end
def new
@resource_id = SecureRandom.uuid
end
def create
ActiveRecord::Base.transaction do
command_bus.call(Domain::CreateResource.new(
resource_id: params[:resource_id],
name: params[:name]
))
end
redirect_to resources_path, notice: "Resource was successfully created"
end
def edit
@resource = ReadModel.find_by_uid(params[:id])
end
def update
command_bus.call(Domain::UpdateResource.new(
resource_id: params[:id],
name: params[:name]
))
redirect_to resource_path(params[:id]), notice: "Resource was successfully updated"
end
def destroy
command_bus.call(Domain::RemoveResource.new(resource_id: params[:id]))
redirect_to resources_path, notice: "Resource was successfully removed"
end
end
5. Controller conventions
Command dispatch:
- Use
command_bus.call(Command.new(...)) to dispatch commands
- Wrap multiple commands in
ActiveRecord::Base.transaction do ... end
- Generate UUIDs before dispatch:
id = SecureRandom.uuid
Read model queries:
- Always use facade methods:
ReadModel.all, ReadModel.find_by_uid(id)
- Never access ActiveRecord models directly
Redirects:
- After successful writes:
redirect_to path, notice: "..."
- After errors:
redirect_to path, alert: "..."
Error handling:
Before actions:
- Use for authorization, ownership checks, loading shared data:
before_action :load_resource, only: [:show, :edit, :update]
private
def load_resource
@resource = ReadModel.find_by_uid(params[:id])
end
6. Form objects (when validation is needed)
For non-trivial forms, create a form object in the controller file or a separate file:
class ResourceForm
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Validations
attribute :name, :string
attribute :price, :decimal
attribute :resource_id, :string
validates :name, presence: true
validates :price, presence: true, numericality: { greater_than: 0 }
end
Use in the controller:
def create
form = ResourceForm.new(**resource_params)
unless form.valid?
return render "new", locals: { errors: form.errors }, status: :unprocessable_entity
end
ActiveRecord::Base.transaction do
command_bus.call(Domain::CreateResource.new(resource_id: form.resource_id, name: form.name))
end
redirect_to resources_path, notice: "Created"
end
7. Service objects (when logic is complex)
When a controller action involves complex coordination (multiple commands, event subscriptions, error handling), extract to a service:
module Namespace
class SubmitService
def initialize(id)
@id = id
end
def call
ActiveRecord::Base.transaction do
command_bus.call(Domain::Submit.new(id: @id))
end
end
private
def command_bus
Rails.configuration.command_bus
end
end
end
8. Namespaced controllers
For controllers under a namespace (e.g. admin/, client/):
module Admin
class ResourcesController < ApplicationController
def index
@resources = ReadModel.all
end
end
end
With a base controller for shared behavior:
module Client
class BaseController < ApplicationController
layout "client_panel"
before_action :ensure_logged_in
private
def ensure_logged_in
end
end
end
module Client
class OrdersController < BaseController
def index
@orders = ClientOrders.orders_for_client(current_client_id)
end
end
end
9. Create views
Create ERB views in app/views/{controller_name}/:
index.html.erb — list view
show.html.erb — detail view
new.html.erb — form for creating
edit.html.erb — form for editing
Views query data via instance variables set in controller (@resources, @resource).
10. Run verification
rails test test/integration/{test_file}.rb — integration tests pass
make test — all tests green
Key conventions
- Controllers are thin — dispatch commands and query read models
- Complex logic goes into service objects
- Multiple commands wrapped in transactions
- UUIDs generated in controller before command dispatch
- Read model facade methods for all queries
- Integration tests exercise the full HTTP stack
- Test-first TDD