agentsclimarketplace

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

Install
npx -y skills add chrisrackstallion/pitchd-rails-cursor-plugin --skill writing-controllers

Assembled 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.

<objective> Write controllers that are thin orchestrators — they receive a request, delegate to the model, and respond. Every custom action maps to CRUD on a new resource. Responses use the simplest Turbo mechanism that works: full-page redirect first, then frames, then streams. Follow DHH/37signals conventions: REST purity, rich models, vanilla controller code, and clarity over cleverness. </objective>

Process

1. Determine What You're Building

TaskAction
New controllerRead references/patterns.md, scaffold the controller
New action on existing controllerRead references/patterns.md § REST Mapping — likely needs a new resource
Turbo / Hotwire responsesRead ../writing-hotwire/references/patterns.md (canonical); references/patterns.md § Response Hierarchy for failed validations
Controller concernRead references/patterns.md § Concerns
Strong parametersRead references/patterns.md § Strong Parameters
AuthorizationRead references/patterns.md § Authorization; see also the policies skill
Error handlingRead references/patterns.md § Error Handling
Code reviewRead 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:

  1. Full-page success — redirect_to after a successful mutation. Turbo Drive advances the page (morphing is a Turbo 8 refinement on some paths, not every visit). Default for ordinary CRUD.
  2. Turbo Frames — scoped regions (drawers, modals, per-record inline edit). Re-render with 422 inside the frame when possible.
  3. 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-PatternInstead
Business logic in actionsModel method or domain verb
Custom verb actions (close, archive)New noun resource with CRUD
params.require(...).permit(...)params.expect(...)
Reaching for Turbo Streams firstRedirect → frames → streams (../writing-hotwire/references/patterns.md)
Unscoped find for nested resourcesScope through parent association
redirect_to after failed validationrender :action, status: :unprocessable_content
Service object wrapping a single model callLet the controller call the model directly
rescue => e in actionsrescue_from at the controller class level
Skipping CSRF / auth checks per-actionDedicated controller with appropriate base class
Inline current_user.admin? permission checksauthorize @record delegates to Pundit policy
Pundit policy checks in before_actionauthorize in the action — use before_action for authentication and finders
Missing authorization in an actionafter_action :verify_authorized catches forgotten calls
Missing policy_scope on indexafter_action :verify_policy_scoped, only: :index catches it

5. Naming Conventions

ThingConventionExamples
ControllersPlural noun matching resourceArticlesController, CommentsController
Nested state controllersParent::StateNounControllerCards::ClosuresController, Articles::PublicationsController
Namespace controllersModule wrappingAdmin::ArticlesController
Callbacksset_ for findersset_article, set_comment
Params methodsresource_paramsarticle_params, comment_params
Resource findersset_ + singular resource nameset_article, set_comment
ConcernsAdjective or Scoped suffixSearchable, 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, not params.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_authorized catches misses
  • Index actions use policy_scope to filter records, then authorize (typically authorize Model after scoping)
  • after_action :verify_policy_scoped, only: :index is set in ApplicationController — 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.

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.