# Controller

> Create a controller with commands, read model queries, routes, and integration tests Use when this capability is needed.

- Skill: `tomevault-io/controller-2` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/controller-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/controller-2/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/controller-2

---


# 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`.

```ruby
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!
    # extract ID from page or use read model facade
    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`:

```ruby
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:**
- Use RESTful `resources` with `only:` to limit actions
- Custom actions go in `member do` (operates on one record) or `collection do` (operates on many)
- Namespace for grouped controllers:
  ```ruby
  namespace :admin do
    resources :stores, only: [:index, :new, :create]
  end
  ```

### 4. Create the controller

```ruby
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:**
- Use `rescue`/`else` for domain exceptions:
  ```ruby
  def submit
    SomeService.new(params[:id]).call
  rescue Domain::SomeError => e
    redirect_to edit_path(params[:id]), alert: e.message
  else
    redirect_to show_path(params[:id]), notice: "Success"
  end
  ```

**Before actions:**
- Use for authorization, ownership checks, loading shared data:
  ```ruby
  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:

```ruby
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:

```ruby
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:

```ruby
# app/services/{namespace}/submit_service.rb
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/`):

```ruby
# app/controllers/admin/resources_controller.rb
module Admin
  class ResourcesController < ApplicationController
    def index
      @resources = ReadModel.all
    end
  end
end
```

With a base controller for shared behavior:

```ruby
# app/controllers/client/base_controller.rb
module Client
  class BaseController < ApplicationController
    layout "client_panel"
    before_action :ensure_logged_in

    private

    def ensure_logged_in
      # auth check
    end
  end
end

# app/controllers/client/orders_controller.rb
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

1. `rails test test/integration/{test_file}.rb` — integration tests pass
2. `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

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/railseventstore) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-11 -->

