Agent Skills: CRUD Patterns (37signals)

>-

UncategorizedID: thibautbaissac/rails_ai_agents/crud-patterns

Install this agent skill to your local

pnpm dlx add-skill https://github.com/ThibautBaissac/rails_ai_agents/tree/HEAD/.claude_37signals/skills/crud-patterns

Skill Files

Browse the full folder contents for crud-patterns.

Download Skill

Loading file tree…

.claude_37signals/skills/crud-patterns/SKILL.md

Skill Metadata

Name
crud-patterns
Description
>-

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:

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:

# 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)

# 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

# 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

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

Module scoping for organization

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

Polymorphic routes with resolve

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:

# 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:

# 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)

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

Multi-format responses

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:

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