.claude_37signals/skills/crud-patterns/SKILL.md
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).
npx skillsauth add ThibautBaissac/rails_ai_agents crud-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Map any action to CRUD. When something doesn't fit standard CRUD, create a new resource.
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
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) |
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
# 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
# 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
resource :closure, only: [:create, :destroy] # No :show, :edit, :new
resources :cards do
scope module: :cards do
resources :comments
resources :attachments
resource :closure
resource :goldness
end
end
resolve "Comment" do |comment, options|
options[:anchor] = ActionView::RecordIdentifier.dom_id(comment)
route_for :card, comment.card, options
end
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
respond_to do |format|
format.turbo_stream
format.html { redirect_to @resource }
end
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
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
app/controllers/[namespace]/[resource]_controller.rbconfig/routes.rbtest/controllers/[namespace]/[resource]_controller_test.rbapp/controllers/concerns/[resource]_scoped.rbdevelopment
Creates Turbo Streams, Turbo Frames, and morphing patterns for real-time UI updates. Use when adding real-time updates, partial page rendering, form submissions, or broadcasting. WHEN NOT: For Stimulus JavaScript controllers (see stimulus-patterns skill). For general view conventions (see rules/views.md).
testing
Writes Minitest tests with fixtures following 37signals conventions. Uses Minitest (not RSpec) and fixtures (not factories). Use when writing tests, adding test coverage, or creating fixtures. WHEN NOT: For RSpec or FactoryBot patterns (this project uses Minitest + fixtures exclusively). For test configuration/CI setup (see project docs).
tools
Builds focused, single-purpose Stimulus controllers for progressive enhancement. Use when adding JavaScript behavior, UI interactions, form enhancements, or building reusable client-side components. WHEN NOT: For Turbo Stream/Frame patterns (see turbo-patterns skill). For server-side view logic (see rules/views.md).
testing
Implements the state-as-records-not-booleans pattern for rich state tracking. Use when modeling state changes, replacing boolean flags with record-based state, or when user mentions state records, closures, publications, or toggling state. WHEN NOT: Technical flags like cached/processed (use booleans), concern extraction (use concern-patterns), general model work (use model-patterns).