# Crud Patterns

> Generates RESTful controllers mapping any action to CRUD by creating new resources instead of custom actions. Use when adding features, creating controllers, designing routes, or handling state changes via REST. WHEN NOT: Non-REST APIs (use api-patterns), view/template work (use turbo-patterns), or model business logic (use model-patterns).

- Skill: `thibautbaissac/crud-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add thibautbaissac/crud-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/thibautbaissac/crud-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: thibautbaissac (https://skillmd.com/u/thibautbaissac)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/thibautbaissac/crud-patterns

---


# CRUD Patterns (37signals)

Map any action to CRUD. When something doesn't fit standard CRUD, create a new resource.

## Project knowledge

**Tech Stack:** Rails 8.2 (edge), Turbo, Stimulus, Solid Queue, MySQL/SQLite
**Routing:** Use `scope module:` for namespacing nested resources
**Controllers:** Thin with concerns for shared behavior

**Commands:**
```bash
bin/rails routes | grep cards              # Check routes
bin/rails generate controller cards/closures  # Generate controller
bin/rails test test/controllers/           # Run controller tests
```

## Resource thinking

When asked to add functionality, ask: "What resource does this represent?"

| User request | Resource to create |
|---|---|
| "Let users close cards" | `Cards::ClosuresController` (create/destroy) |
| "Let users mark important" | `Cards::GoldnessesController` (create/destroy) |
| "Let users follow a card" | `Cards::WatchesController` (create/destroy) |
| "Let users assign cards" | `Cards::AssignmentsController` (create/destroy) |
| "Let users publish boards" | `Boards::PublicationsController` (create/destroy) |
| "Let users position cards" | `Cards::PositionsController` (update) |
| "Let users archive projects" | `Projects::ArchivalsController` (create/destroy) |

## State change controllers (singular resources)

Toggle state via POST (create) and DELETE (destroy) on a singular resource:

```ruby
# app/controllers/cards/closures_controller.rb
class Cards::ClosuresController < ApplicationController
  include CardScoped  # Provides @card, @board

  def create
    @card.close
    render_card_replacement
  end

  def destroy
    @card.reopen
    render_card_replacement
  end
end
```

## Standard CRUD controllers (plural resources)

```ruby
# app/controllers/cards/comments_controller.rb
class Cards::CommentsController < ApplicationController
  include CardScoped

  def index
    @comments = @card.comments.recent
  end

  def create
    @comment = @card.comments.create!(comment_params)

    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to @card }
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:body)
  end
end
```

## Nested resource controllers

```ruby
# app/controllers/boards/columns_controller.rb
class Boards::ColumnsController < ApplicationController
  include BoardScoped

  def show
    @column = @board.columns.find(params[:id])
    @cards = @column.cards.positioned
  end

  def create
    @column = @board.columns.create!(column_params)

    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to @board }
    end
  end

  def update
    @column = @board.columns.find(params[:id])
    @column.update!(column_params)
    head :no_content
  end

  def destroy
    @column = @board.columns.find(params[:id])
    @column.destroy!

    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to @board }
    end
  end

  private

  def column_params
    params.require(:column).permit(:name, :position)
  end
end
```

## Routing patterns

### Singular resource for toggles

```ruby
resource :closure, only: [:create, :destroy]  # No :show, :edit, :new
```

### Module scoping for organization

```ruby
resources :cards do
  scope module: :cards do
    resources :comments
    resources :attachments
    resource :closure
    resource :goldness
  end
end
```

### Polymorphic routes with resolve

```ruby
resolve "Comment" do |comment, options|
  options[:anchor] = ActionView::RecordIdentifier.dom_id(comment)
  route_for :card, comment.card, options
end
```

## Scoping concerns

Provide parent resource lookup for nested controllers:

```ruby
# app/controllers/concerns/card_scoped.rb
module CardScoped
  extend ActiveSupport::Concern

  included do
    before_action :set_card
    before_action :set_board
  end

  private

  def set_card
    @card = Current.account.cards.find(params[:card_id])
  end

  def set_board
    @board = @card.board
  end

  def render_card_replacement
    respond_to do |format|
      format.turbo_stream do
        render turbo_stream: turbo_stream.replace(
          dom_id(@card, :card_container),
          partial: "cards/container",
          locals: { card: @card.reload }
        )
      end
      format.html { redirect_to @card }
    end
  end
end
```

Create new scoping concerns as needed:

```ruby
# app/controllers/concerns/project_scoped.rb
module ProjectScoped
  extend ActiveSupport::Concern

  included do
    before_action :set_project
  end

  private

  def set_project
    @project = Current.account.projects.find(params[:project_id])
  end
end
```

## Response patterns

### Turbo Stream responses (preferred)

```ruby
respond_to do |format|
  format.turbo_stream
  format.html { redirect_to @resource }
end
```

### Multi-format responses

```ruby
def create
  @resource = Model.create!(resource_params)

  respond_to do |format|
    format.turbo_stream
    format.html { redirect_to @resource }
    format.json { render json: @resource, status: :created, location: @resource }
  end
end
```

## Authorization pattern

Keep authorization checks thin, delegate logic to models:

```ruby
before_action :ensure_can_administer_card, only: [:destroy]

private

def ensure_can_administer_card
  head :forbidden unless Current.user.can_administer_card?(@card)
end
```

## Files to create for a new resource

1. **Controller:** `app/controllers/[namespace]/[resource]_controller.rb`
2. **Route entry:** Add to `config/routes.rb`
3. **Test:** `test/controllers/[namespace]/[resource]_controller_test.rb`
4. **Concern (if needed):** `app/controllers/concerns/[resource]_scoped.rb`

## Boundaries

- **Always:** Map actions to CRUD, create new resources for state changes, use concerns for scoping, only 7 REST actions, use strong parameters
- **Ask first:** Before adding custom actions, before creating non-REST routes, before modifying routing constraints
- **Never:** Add member/collection routes, create controllers without tests, put business logic in controllers

