home / skills / thibautbaissac / rails_ai_agents / rails-controller

rails-controller skill

/skills/rails-controller

This skill helps you scaffold Rails controllers using a TDD workflow, writing red specs first and delivering fully tested actions.

npx playbooks add skill thibautbaissac/rails_ai_agents --skill rails-controller

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.6 KB
---
name: rails-controller
description: Creates Rails controllers with TDD approach - request spec first, then implementation. Use when creating new controllers, adding controller actions, implementing CRUD operations, or when user mentions controllers, routes, or API endpoints.
allowed-tools: Read, Write, Edit, Bash(bundle exec rspec:*), Glob, Grep
---

# Rails Controller Generator (TDD)

Creates RESTful controllers following project conventions with request specs first.

## Quick Start

1. Write failing request spec in `spec/requests/`
2. Run spec to confirm RED
3. Implement controller action
4. Run spec to confirm GREEN
5. Refactor if needed

## Project Conventions

This project uses:
- **Pundit** for authorization (`authorize @resource`, `policy_scope(Model)`)
- **Pagy** for pagination
- **Presenters** for view formatting
- **Multi-tenancy** via `current_account`
- **Turbo Stream** responses for dynamic updates

## TDD Workflow

### Step 1: Create Request Spec (RED)

```ruby
# spec/requests/[resources]_spec.rb
RSpec.describe "[Resources]", type: :request do
  let(:user) { create(:user) }
  let(:other_user) { create(:user) }

  before { sign_in user, scope: :user }

  describe "GET /[resources]" do
    let!(:resource) { create(:[resource], account: user.account) }
    let!(:other_resource) { create(:[resource], account: other_user.account) }

    it "returns http success" do
      get [resources]_path
      expect(response).to have_http_status(:success)
    end

    it "shows only current_user's resources (multi-tenant)" do
      get [resources]_path
      expect(response.body).to include(resource.name)
      expect(response.body).not_to include(other_resource.name)
    end
  end

  describe "GET /[resources]/:id" do
    let!(:resource) { create(:[resource], account: user.account) }

    it "returns http success" do
      get [resource]_path(resource)
      expect(response).to have_http_status(:success)
    end
  end

  describe "POST /[resources]" do
    let(:valid_params) { { [resource]: attributes_for(:[resource]) } }

    it "creates a new resource" do
      expect {
        post [resources]_path, params: valid_params
      }.to change([Resource], :count).by(1)
    end

    it "assigns to current_account" do
      post [resources]_path, params: valid_params
      expect([Resource].last.account).to eq(user.account)
    end
  end

  describe "authorization" do
    let!(:other_resource) { create(:[resource], account: other_user.account) }

    it "returns 404 for unauthorized access" do
      get [resource]_path(other_resource)
      expect(response).to have_http_status(:not_found)
    end
  end
end
```

### Step 2: Run Spec (Confirm RED)

```bash
bundle exec rspec spec/requests/[resources]_spec.rb
```

### Step 3: Implement Controller (GREEN)

```ruby
# app/controllers/[resources]_controller.rb
class [Resources]Controller < ApplicationController
  before_action :set_[resource], only: [:show, :edit, :update, :destroy]

  def index
    authorize [Resource], :index?
    @pagy, resources = pagy(policy_scope([Resource]).order(created_at: :desc))
    @[resources] = resources.map { |r| [Resource]Presenter.new(r) }
  end

  def show
    authorize @[resource]
    @[resource] = [Resource]Presenter.new(@[resource])
  end

  def new
    @[resource] = current_account.[resources].build
    authorize @[resource]
  end

  def create
    @[resource] = current_account.[resources].build([resource]_params)
    authorize @[resource]

    if @[resource].save
      redirect_to [resources]_path, notice: "[Resource] created successfully"
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    authorize @[resource]
  end

  def update
    authorize @[resource]

    if @[resource].update([resource]_params)
      redirect_to @[resource], notice: "[Resource] updated successfully"
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    authorize @[resource]
    @[resource].destroy
    redirect_to [resources]_path, notice: "[Resource] deleted successfully"
  end

  private

  def set_[resource]
    @[resource] = policy_scope([Resource]).find(params[:id])
  end

  def [resource]_params
    params.require(:[resource]).permit(:name, :field1, :field2)
  end
end
```

### Step 4: Run Spec (Confirm GREEN)

```bash
bundle exec rspec spec/requests/[resources]_spec.rb
```

## Namespaced Controllers

For nested routes like `settings/accounts`:

```ruby
# app/controllers/settings/accounts_controller.rb
module Settings
  class AccountsController < ApplicationController
    before_action :set_account

    def show
      authorize @account
    end

    private

    def set_account
      @account = current_account
    end
  end
end
```

## Turbo Stream Response Pattern

```ruby
def create
  @resource = current_account.resources.build(resource_params)
  authorize @resource

  if @resource.save
    respond_to do |format|
      format.html { redirect_to resources_path, notice: "Created" }
      format.turbo_stream do
        flash.now[:notice] = "Created"
        @pagy, @resources = pagy(policy_scope(Resource).order(created_at: :desc))
        render turbo_stream: [
          turbo_stream.replace("resources-list", partial: "resources/list"),
          turbo_stream.update("modal", "")
        ]
      end
    end
  else
    render :new, status: :unprocessable_entity
  end
end
```

## Checklist

- [ ] Request spec written first (RED)
- [ ] Multi-tenant isolation tested
- [ ] Authorization tested (404 for unauthorized)
- [ ] Controller uses `authorize` on every action
- [ ] Controller uses `policy_scope` for queries
- [ ] Presenter wraps models for views
- [ ] Strong parameters defined
- [ ] All specs GREEN

Overview

This skill creates Rails controllers using a test-driven workflow: write a failing request spec first, implement the controller, then make the spec pass. It follows project conventions like Pundit for authorization, Pagy for pagination, presenters for view formatting, multi-tenancy via current_account, and Turbo Stream responses for dynamic updates.

How this skill works

I generate a request spec under spec/requests to define expected behavior and multi-tenant isolation. After you run the spec and see RED, I scaffold the controller actions with authorize calls, policy_scope queries, strong params, presenter wrapping, and Turbo Stream responses where applicable. Run the specs again to confirm GREEN, then refactor while keeping tests passing.

When to use it

  • Adding a new controller or resource-backed CRUD endpoints
  • Implementing or changing controller actions for existing resources
  • Creating API endpoints that require multi-tenant scoping and authorization
  • When you want strict TDD: request spec first, implementation next
  • Converting views to Turbo Stream responses for dynamic updates

Best practices

  • Write the request spec first and confirm it fails before coding (RED)
  • Always use policy_scope for queries and authorize every resource/action
  • Ensure created records are assigned to current_account for multi-tenancy
  • Wrap models in presenters for view formatting instead of placing logic in controllers
  • Use Pagy for pagination and return Turbo Stream responses for progressive UX

Example use cases

  • Create a PostsController: spec/requests/posts_spec.rb then implement index, show, create, update, destroy
  • Add an admin namespaced controller under app/controllers/settings/accounts_controller.rb with current_account
  • Implement a create action that returns html and turbo_stream formats with pagy re-rendering
  • Fix an authorization bug: add tests expecting 404 for resources outside current_account
  • Add strong params and presenters while keeping request specs green

FAQ

Do I always need a request spec first?

Yes. The workflow relies on writing a failing request spec to drive the controller implementation.

How are unauthorized accesses handled?

Tests should assert a 404 for resources outside current_account; controllers must use policy_scope to limit finds and authorize on each action.