$npx -y skills add ThibautBaissac/rails_ai_agents --skill crud-patternsGenerates 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
| 1 | # CRUD Patterns (37signals) |
| 2 | |
| 3 | Map any action to CRUD. When something doesn't fit standard CRUD, create a new resource. |
| 4 | |
| 5 | ## Project knowledge |
| 6 | |
| 7 | **Tech Stack:** Rails 8.2 (edge), Turbo, Stimulus, Solid Queue, MySQL/SQLite |
| 8 | **Routing:** Use `scope module:` for namespacing nested resources |
| 9 | **Controllers:** Thin with concerns for shared behavior |
| 10 | |
| 11 | **Commands:** |
| 12 | ```bash |
| 13 | bin/rails routes | grep cards # Check routes |
| 14 | bin/rails generate controller cards/closures # Generate controller |
| 15 | bin/rails test test/controllers/ # Run controller tests |
| 16 | ``` |
| 17 | |
| 18 | ## Resource thinking |
| 19 | |
| 20 | When asked to add functionality, ask: "What resource does this represent?" |
| 21 | |
| 22 | | User request | Resource to create | |
| 23 | |---|---| |
| 24 | | "Let users close cards" | `Cards::ClosuresController` (create/destroy) | |
| 25 | | "Let users mark important" | `Cards::GoldnessesController` (create/destroy) | |
| 26 | | "Let users follow a card" | `Cards::WatchesController` (create/destroy) | |
| 27 | | "Let users assign cards" | `Cards::AssignmentsController` (create/destroy) | |
| 28 | | "Let users publish boards" | `Boards::PublicationsController` (create/destroy) | |
| 29 | | "Let users position cards" | `Cards::PositionsController` (update) | |
| 30 | | "Let users archive projects" | `Projects::ArchivalsController` (create/destroy) | |
| 31 | |
| 32 | ## State change controllers (singular resources) |
| 33 | |
| 34 | Toggle state via POST (create) and DELETE (destroy) on a singular resource: |
| 35 | |
| 36 | ```ruby |
| 37 | # app/controllers/cards/closures_controller.rb |
| 38 | class Cards::ClosuresController < ApplicationController |
| 39 | include CardScoped # Provides @card, @board |
| 40 | |
| 41 | def create |
| 42 | @card.close |
| 43 | render_card_replacement |
| 44 | end |
| 45 | |
| 46 | def destroy |
| 47 | @card.reopen |
| 48 | render_card_replacement |
| 49 | end |
| 50 | end |
| 51 | ``` |
| 52 | |
| 53 | ## Standard CRUD controllers (plural resources) |
| 54 | |
| 55 | ```ruby |
| 56 | # app/controllers/cards/comments_controller.rb |
| 57 | class Cards::CommentsController < ApplicationController |
| 58 | include CardScoped |
| 59 | |
| 60 | def index |
| 61 | @comments = @card.comments.recent |
| 62 | end |
| 63 | |
| 64 | def create |
| 65 | @comment = @card.comments.create!(comment_params) |
| 66 | |
| 67 | respond_to do |format| |
| 68 | format.turbo_stream |
| 69 | format.html { redirect_to @card } |
| 70 | end |
| 71 | end |
| 72 | |
| 73 | private |
| 74 | |
| 75 | def comment_params |
| 76 | params.require(:comment).permit(:body) |
| 77 | end |
| 78 | end |
| 79 | ``` |
| 80 | |
| 81 | ## Nested resource controllers |
| 82 | |
| 83 | ```ruby |
| 84 | # app/controllers/boards/columns_controller.rb |
| 85 | class Boards::ColumnsController < ApplicationController |
| 86 | include BoardScoped |
| 87 | |
| 88 | def show |
| 89 | @column = @board.columns.find(params[:id]) |
| 90 | @cards = @column.cards.positioned |
| 91 | end |
| 92 | |
| 93 | def create |
| 94 | @column = @board.columns.create!(column_params) |
| 95 | |
| 96 | respond_to do |format| |
| 97 | format.turbo_stream |
| 98 | format.html { redirect_to @board } |
| 99 | end |
| 100 | end |
| 101 | |
| 102 | def update |
| 103 | @column = @board.columns.find(params[:id]) |
| 104 | @column.update!(column_params) |
| 105 | head :no_content |
| 106 | end |
| 107 | |
| 108 | def destroy |
| 109 | @column = @board.columns.find(params[:id]) |
| 110 | @column.destroy! |
| 111 | |
| 112 | respond_to do |format| |
| 113 | format.turbo_stream |
| 114 | format.html { redirect_to @board } |
| 115 | end |
| 116 | end |
| 117 | |
| 118 | private |
| 119 | |
| 120 | def column_params |
| 121 | params.require(:column).permit(:name, :position) |
| 122 | end |
| 123 | end |
| 124 | ``` |
| 125 | |
| 126 | ## Routing patterns |
| 127 | |
| 128 | ### Singular resource for toggles |
| 129 | |
| 130 | ```ruby |
| 131 | resource :closure, only: [:create, :destroy] # No :show, :edit, :new |
| 132 | ``` |
| 133 | |
| 134 | ### Module scoping for organization |
| 135 | |
| 136 | ```ruby |
| 137 | resources :cards do |
| 138 | scope module: :cards do |
| 139 | resources :comments |
| 140 | resources :attachments |
| 141 | resource :closure |
| 142 | resource :goldness |
| 143 | end |
| 144 | end |
| 145 | ``` |
| 146 | |
| 147 | ### Polymorphic routes with resolve |
| 148 | |
| 149 | ```ruby |
| 150 | resolve "Comment" do |comment, options| |
| 151 | options[:anchor] = ActionView::RecordIdentifier.dom_id(comment) |
| 152 | route_for :card, comment.card, options |
| 153 | end |
| 154 | ``` |
| 155 | |
| 156 | ## Scoping concerns |
| 157 | |
| 158 | Provide parent resource lookup for nested controllers: |
| 159 | |
| 160 | ```ruby |
| 161 | # app/controllers/concerns/card_scoped.rb |
| 162 | module CardScoped |
| 163 | extend ActiveSupport::Concern |
| 164 | |
| 165 | included do |
| 166 | before_action :set_card |
| 167 | before_action :set_board |
| 168 | end |
| 169 | |
| 170 | private |
| 171 | |
| 172 | def set_card |
| 173 | @card = Current.account.cards.find(params[:card_id]) |
| 174 | end |
| 175 | |
| 176 | def set_board |
| 177 | @board = @card.board |
| 178 | end |
| 179 | |
| 180 | def render_card_replacement |
| 181 | respond_to do |format| |
| 182 | format.turbo_stream do |
| 183 | render turbo_stream: turbo_stream.replace( |
| 184 | dom_id(@card, :card_container), |
| 185 | partial: "cards/container", |
| 186 | locals: { card: @card.reload } |
| 187 | ) |
| 188 | end |
| 189 | format.html { redirect_to @card } |
| 190 | end |
| 191 | end |
| 192 | end |
| 193 | ``` |
| 194 | |
| 195 | Create new scoping concerns as needed: |
| 196 | |
| 197 | ```ruby |
| 198 | # app/controllers/concerns/project_scoped.rb |
| 199 | module ProjectScoped |
| 200 | extend ActiveSupport::Concern |
| 201 | |
| 202 | included do |
| 203 | before_action :set_project |
| 204 | end |
| 205 | |
| 206 | private |
| 207 | |
| 208 | def set_project |
| 209 | @ |