Rails_ai_agents crud-patterns
install
source · Clone the upstream repo
git clone https://github.com/ThibautBaissac/rails_ai_agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ThibautBaissac/rails_ai_agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude_37signals/skills/crud-patterns" ~/.claude/skills/thibautbaissac-rails-ai-agents-crud-patterns && rm -rf "$T"
manifest:
.claude_37signals/skills/crud-patterns/SKILL.mdsource content
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" | (create/destroy) |
| "Let users mark important" | (create/destroy) |
| "Let users follow a card" | (create/destroy) |
| "Let users assign cards" | (create/destroy) |
| "Let users publish boards" | (create/destroy) |
| "Let users position cards" | (update) |
| "Let users archive projects" | (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
- 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