Writing controllers
Skill chrisrackstallion/pitchd-rails-cursor-plugin/skills/writing-controllers
Write Rails controllers following DHH/37signals conventions — thin CRUD controllers, REST-mapped resources, redirect-first Hotwire responses, concerns for shared behaviour, and strong parameters with params.expect. Use when creating new controllers, adding actions, extracting concerns, handling Turbo responses, or when the user mentions controllers, actions, strong parameters, or REST routing.From its SKILL.md
npx -y skills add chrisrackstallion/pitchd-rails-cursor-plugin --skill writing-controllersAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
8.2 KB, as published. Nobody here has run it
Writing Rails Controllers
Reading plugin files: Before your first Read of rules/*.mdc or a
sibling skills/*/SKILL.md, resolve the correct path prefix via
resolving-plugin-root — these paths (bare or with ../) only resolve correctly
against Cursor's workspace root or a raw checkout; a Claude Code plugin
install needs the resolved prefix instead.
Process
1. Determine What You're Building
| Task | Action |
|---|---|
| New controller | Read references/patterns.md, scaffold the controller |
| New action on existing controller | Read references/patterns.md § REST Mapping — likely needs a new resource |
| Turbo / Hotwire responses | Read ../writing-hotwire/references/patterns.md (canonical); references/patterns.md § Response Hierarchy for failed validations |
| Controller concern | Read references/patterns.md § Concerns |
| Strong parameters | Read references/patterns.md § Strong Parameters |
| Authorization | Read references/patterns.md § Authorization; see also the policies skill |
| Error handling | Read references/patterns.md § Error Handling |
| Code review | Read all references, review against conventions |
2. Controller Structure
Organise every controller in this order:
class ArticlesController < ApplicationController
# 1. Concerns
include Sortable
# 2. Security / rate limiting
rate_limit to: 10, within: 3.minutes, only: :create
# 3. Callbacks
before_action :set_article, only: %i[ show edit update destroy ]
# 4. Public actions — in CRUD order
def index
@articles = policy_scope(Article).chronologically
authorize Article
end
def show
authorize @article
end
def new
@article = Article.new
authorize @article
end
def create
@article = Article.new(article_params)
authorize @article
if @article.save
redirect_to @article, notice: "Article created."
else
render :new, status: :unprocessable_content
end
end
def edit
authorize @article
end
def update
authorize @article
if @article.update(article_params)
redirect_to @article, notice: "Article updated."
else
render :edit, status: :unprocessable_content
end
end
def destroy
authorize @article
@article.destroy!
redirect_to articles_path, notice: "Article deleted."
end
private
# 5. Resource finders
def set_article
@article = Article.find(params[:id])
end
# 6. Strong parameters
def article_params
params.expect(article: %i[ title body visibility ])
end
end
This is the baseline. It uses redirect_to for every mutation.
No respond_to, no format negotiation, no stream templates. Stay here
as long as possible.
Flash copy is hardcoded here to keep the example focused — in application
code, flash strings live in config/locales (e.g.
notice: t("articles.created_notice"); see the i18n rule).
3. Decision Framework
Before writing code, ask these questions:
"Does this need a new controller or a new action?"
- Action outside CRUD (close, archive, publish) → new noun resource with its own controller
- Standard CRUD operation → action on existing controller
- Shared behaviour across controllers → concern
"Where does the logic live?"
- Single-model operation → model method, controller just calls it
- Multi-model orchestration → controller coordinates, each model does its part
- External system call → job enqueued by controller or model callback
- Query/filtering → named scope on the model
"What response mechanism?"
Prefer the simplest mechanism that satisfies the UX. Each step adds
controller complexity — maximise use of redirect_to:
- Full-page success —
redirect_toafter a successful mutation. Turbo Drive advances the page (morphing is a Turbo 8 refinement on some paths, not every visit). Default for ordinary CRUD. - Turbo Frames — scoped regions (drawers, modals, per-record inline edit). Re-render with 422 inside the frame when possible.
- Turbo Streams — multi-target DOM updates. Last resort.
Details match rules/controllers.mdc § Response Hierarchy (Hotwire). Frames, streams, Stimulus, and anti-SPA patterns: ../writing-hotwire/references/patterns.md.
4. Anti-Patterns
| Anti-Pattern | Instead |
|---|---|
| Business logic in actions | Model method or domain verb |
Custom verb actions (close, archive) | New noun resource with CRUD |
params.require(...).permit(...) | params.expect(...) |
| Reaching for Turbo Streams first | Redirect → frames → streams (../writing-hotwire/references/patterns.md) |
| Unscoped find for nested resources | Scope through parent association |
redirect_to after failed validation | render :action, status: :unprocessable_content |
| Service object wrapping a single model call | Let the controller call the model directly |
rescue => e in actions | rescue_from at the controller class level |
| Skipping CSRF / auth checks per-action | Dedicated controller with appropriate base class |
Inline current_user.admin? permission checks | authorize @record delegates to Pundit policy |
Pundit policy checks in before_action | authorize in the action — use before_action for authentication and finders |
| Missing authorization in an action | after_action :verify_authorized catches forgotten calls |
Missing policy_scope on index | after_action :verify_policy_scoped, only: :index catches it |
5. Naming Conventions
| Thing | Convention | Examples |
|---|---|---|
| Controllers | Plural noun matching resource | ArticlesController, CommentsController |
| Nested state controllers | Parent::StateNounController | Cards::ClosuresController, Articles::PublicationsController |
| Namespace controllers | Module wrapping | Admin::ArticlesController |
| Callbacks | set_ for finders | set_article, set_comment |
| Params methods | resource_params | article_params, comment_params |
| Resource finders | set_ + singular resource name | set_article, set_comment |
| Concerns | Adjective or Scoped suffix | Searchable, BoardScoped, Sortable |
6. Verification
Before finishing, verify:
- No business logic in controller actions — only orchestration
- Every action maps to one of the seven RESTful actions (index, show, new, create, edit, update, destroy)
- Strong parameters use
params.expect, notparams.require.permit - Failed validations render with
status: :unprocessable_content, not redirect - Nested resource lookups are scoped through parent associations
- Response uses the simplest Turbo mechanism (full-page redirect → frames → streams — see
../writing-hotwire/references/patterns.md) - Every action calls
authorize(Pundit) — including index —after_action :verify_authorizedcatches misses - Index actions use
policy_scopeto filter records, thenauthorize(typicallyauthorize Modelafter scoping) -
after_action :verify_policy_scoped, only: :indexis set inApplicationController— it is required, not optional - Controller has no more than ~7 public methods (CRUD actions only)
- Shared behaviour is extracted to concerns, not duplicated
- Rate limiting applied to creation/mutation endpoints where appropriate
References
For detailed patterns and examples, see references/patterns.md.