crud-patterns
Skill211 starsChanged 5 months agoMITView on GitHub
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).
In this repository17 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .claude/CLAUDE.mdCLAUDE.md
- .opencode/CLAUDE.mdCLAUDE.md
- api-patternsSkill
- auth-setupSkill
- caching-patternsSkill
- concern-patternsSkill
- event-trackingSkill
- job-patternsSkill
- mailer-patternsSkill
- migration-patternsSkill
- model-patternsSkill
- multi-tenant-setupSkill
- state-recordsSkill
- stimulus-patternsSkill
- testing-patternsSkill
- turbo-patternsSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 5 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 20
Routing: Use scope module: for namespacing nested resources
- Distinctive: line 32
When asked to add functionality, ask: "What resource does this represent?"
- Distinctive: line 207
Create new scoping concerns as needed:
- Distinctive: line 253
Keep authorization checks thin, delegate logic to models:
- Very specific: line 268
Route entry: Add to config/routes.rb
- Distinctive: line 275
Ask first: Before adding custom actions, before creating non-REST routes, before modifying routing constraints
- Distinctive: line 276
Never: Add member/collection routes, create controllers without tests, put business logic in controllers
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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 testsResource 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
endStandard 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
endNested 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
endRouting patterns
Singular resource for toggles
resource :closure, only: [:create, :destroy] # No :show, :edit, :newModule scoping for organization
resources :cards do
scope module: :cards do
resources :comments
resources :attachments
resource :closure
resource :goldness
end
endPolymorphic routes with resolve
resolve "Comment" do |comment, options|
options[:anchor] = ActionView::RecordIdentifier.dom_id(comment)
route_for :card, comment.card, options
endScoping 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
endCreate 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
endResponse patterns
Turbo Stream responses (preferred)
respond_to do |format|
format.turbo_stream
format.html { redirect_to @resource }
endMulti-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
endAuthorization 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)
endFiles to create for a new resource
- Controller:
app/controllers/[namespace]/[resource]_controller.rb - Route entry: Add to
config/routes.rb - Test:
test/controllers/[namespace]/[resource]_controller_test.rb - 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
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

