mcpField
A
action-controller

action-controller

github.com/thinkoodle開発7191 回インストール8 週間前に更新
リポジトリ

action-controller について

Rails 8.1 の Action Controller 向けスキルです。開発者やコーディングエージェントが、安全で慣例に沿ったコントローラーを作成できるよう、パラメーター処理や応答などを案内します。

主な機能

  1. 01Rails 8.1 のコントローラー規約
  2. 02強パラメーターとネスト構造
  3. 03コールバック順序と認可
  4. 04描画、リダイレクト、セッション
  5. 05CSRF とエラー処理の指針

活用シーン

  1. 01CRUD アクションの作成
  2. 02ネストされたフォーム値の絞り込み
  3. 03認証コールバックの追加
  4. 04エラーや Turbo 応答の処理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/thinkoodle/rails-skills --skill action-controller

インストール後、Claude Code で/action-controllerを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
callbacks.md
rendering.md
security.md
sessions-and-cookies.md
streaming.md
strong-params.md

詳細

Rails Action Controller エキスパート

Rails 8.1 の規約に従って、正しく、安全で、慣用的な Rails コントローラーを記述します。

設計理念

  1. 薄いコントローラー — ビジネスロジックはコントローラーではなく、モデルやサービスに置きます
  2. ストロングパラメーターでマスアサインメントを防ぐ — 生の params を使うと、攻撃者が任意の属性(管理者フラグ、ユーザー ID など)を設定できます
  3. 設定より規約 — RESTful なパターンに従い、カスタムアクションを追加したくなる衝動を抑えます
  4. 安全側に倒す — まずアクセスを制限し、必要な範囲だけ選択的に許可します
  5. 1 つのコントローラーで 1 つのリソース — コントローラーが 2 つのリソースを扱う場合は、分割します

このスキルを使う場面

  • 新しいコントローラーアクション(CRUD またはカスタムアクション)を記述するとき
  • パラメーターを許可するとき(特にネストしたハッシュや配列。ここでバグが発生しやすくなります)
  • 認証/認可のための before_action フィルターを追加するとき
  • エラー処理のために rescue_from を設定するとき
  • セッション、Cookie、フラッシュメッセージを扱うとき
  • レスポンスをレンダリング、またはリダイレクトするとき
  • ストリーミングやファイルのダウンロードを実装するとき
  • CSRF 保護や HTTP 認証を設定するとき

手順

手順 1:既存のパターンを確認する

まずプロジェクト内の既存のコントローラーを確認してください — 教科書的なパターンより、コードベースとの一貫性が重要です。

# See what ApplicationController provides
cat app/controllers/application_controller.rb

# Find similar controllers
ls app/controllers/

# Check for shared concerns
ls app/controllers/concerns/

# Check routes for the resource
bin/rails routes | grep resource_name

一貫性は「ベストプラクティス」に勝ります。

手順 2:コントローラーの構成

すべてのコントローラーで、次の順序に従ってください。

class ArticlesController < ApplicationController
  # 1. Includes/concerns
  include Searchable

  # 2. Constants (if any)
  ITEMS_PER_PAGE = 25

  # 3. Callbacks — order matters, they run top-to-bottom
  before_action :authenticate_user!
  before_action :set_article, only: [:show, :edit, :update, :destroy]
  before_action :authorize_article, only: [:edit, :update, :destroy]

  # 4. Public actions (RESTful order: index, show, new, create, edit, update, destroy)
  def index
    @articles = Article.all
  end

  def show; end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)
    if @article.save
      redirect_to @article, notice: "Article created."
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit; end

  def update
    if @article.update(article_params)
      redirect_to @article, notice: "Article updated."
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @article.destroy!
    redirect_to articles_path, notice: "Article deleted.", status: :see_other
  end

  # 5. Private methods
  private

    def set_article
      @article = Article.find(params.expect(:id))
    end

    def authorize_article
      redirect_to articles_path, alert: "Not authorized." unless @article.user == current_user
    end

    def article_params
      params.expect(article: [:title, :body, :published])
    end
end
手順 3:ストロングパラメーター

コントローラーのバグの多くはここから発生します。特に、ネストしたハッシュや配列で多く発生します。

require + permit より expect(Rails 8 以降)を使う
# ✅ Rails 8+ — prefer expect (combines require + permit)
def article_params
  params.expect(article: [:title, :body, :published])
end

# ⚠️ Older style — still works but expect is preferred
def article_params
  params.require(:article).permit(:title, :body, :published)
end
ネストしたハッシュ — 危険な領域
# Nested hash (belongs_to address, has fields)
# Form sends: { user: { name: "Jo", address: { street: "123 Main", city: "NY" } } }
def user_params
  params.expect(user: [:name, address: [:street, :city, :zip]])
end

# ⚠️ Wrong — this permits NOTHING inside address
def user_params
  params.expect(user: [:name, :address])  # address is a hash, not a scalar!
end
スカラー値の配列
# Array of simple values: { article: { title: "Hi", tag_ids: [1, 2, 3] } }
def article_params
  params.expect(article: [:title, tag_ids: []])
end
ハッシュの配列(accepts_nested_attributes_for)
# Array of nested objects — NOTE THE DOUBLE ARRAY SYNTAX [[...]]
# { project: { name: "X", tasks_attributes: [{ title: "A" }, { title: "B" }] } }
def project_params
  params.expect(project: [:name, tasks_attributes: [[:title, :done, :id, :_destroy]]])
end

二重配列 [[...]] は、「指定したキーを持つハッシュの配列を期待する」という意味です。 これは Rails 8 の expect 構文です。エージェントはこれを頻繁に間違えます。

任意のハッシュ(使用は控えめに)
# When you genuinely can't enumerate keys (e.g., JSON metadata blob)
def product_params
  params.expect(product: [:name, metadata: {}])
end
# ⚠️ metadata: {} permits ANY keys — only use when truly dynamic
よくある落とし穴
バグ修正
tags が配列なのに permit(:tags) を使うpermit(tags: [])
address がハッシュなのに permit(:address) を使うpermit(address: [:street, :city])
ファイルアップロードに permit(:images) を使う複数ファイルの場合は permit(images: [])
_destroy と id を含めずにネストした属性を使うpermit(items_attributes: [[:name, :id, :_destroy]])
「とにかく動かす」ために permit! を使うパラメーターを列挙してください — permit! は攻撃者が任意の属性を設定できるようにします
手順 4:コールバック(before_action、after_action、around_action)
before_action
class PostsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  # Skip inherited callbacks selectively
  skip_before_action :authenticate_user!, only: [:index, :show]

  private
    def set_post
      @post = Post.find(params.expect(:id))
    end
end

主なルール:

  • コールバックは宣言順に実行されます。リソースの読み込みより先に認証を行ってください
  • コールバックでレンダリングまたはリダイレクトすると、後続のチェーンが停止します(残りのコールバックとアクションは実行されません)
  • only: / except: を使って、特定のアクションにコールバックを適用します
  • skip_before_action が機能するのは、親クラスから継承されたコールバック、または先に登録されたコールバックに対してのみです
around_action
around_action :wrap_in_transaction, only: [:create, :update]

private
  def wrap_in_transaction
    ActiveRecord::Base.transaction do
      yield  # executes the action
    end
  end

yield は必須です — これがないとアクションは実行されません。

手順 5:レスポンスのレンダリング
# Implicit render — renders app/views/posts/index.html.erb
def index
  @posts = Post.all
  # no explicit render needed
end

# Explicit render of a different template
render :new                           # same controller, different action template
render "posts/new"                    # cross-controller template
render plain: "OK"                    # plain text
render json: @post                    # JSON
render json: @post, status: :created  # JSON with status
render html: "<h1>Hi</h1>".html_safe  # raw HTML
render inline: "<%= 'hi' %>"          # inline ERB (avoid)
render nothing: true, status: :ok      # empty body (deprecated — use head)

# head — response with no body
head :no_content          # 204
head :created, location: post_url(@post)

# Render with status (Turbo requires it for error responses)
render :new, status: :unprocessable_entity   # 422 — required for Turbo
render :edit, status: :unprocessable_entity

Turbo/Hotwire の場合: フォーム送信に失敗したときは status: :unprocessable_entity(422)が必要です。これがないと、Turbo はレスポンスを無視し、ユーザーにエラーが表示されません。

複数の形式に対応する respond_to
def show
  @post = Post.find(params.expect(:id))

  respond_to do |format|
    format.html  # renders show.html.erb
    format.json { render json: @post }
    format.pdf { send_data generate_pdf(@post), filename: "post.pdf" }
  end
end
手順 6:リダイレクト
redirect_to @post                          # redirect to show
redirect_to posts_path                     # redirect to index
redirect_to root_path, notice: "Done!"     # with flash notice
redirect_to root_path, alert: "Oops!"      # with flash alert
redirect_to root_path, status: :see_other  # 303 — use for DELETE actions

# Redirect back (with fallback)
redirect_back fallback_location: root_path

# ⚠️ After DELETE, use status: :see_other (303)
# This prevents browsers from replaying the DELETE on redirect
def destroy
  @post.destroy!
  redirect_to posts_path, notice: "Deleted.", status: :see_other
end
手順 7:フラッシュメッセージ
# Set in redirect
redirect_to @post, notice: "Saved!"
redirect_to @post, alert: "Problem!"

# Set manually (available on NEXT request)
flash[:notice] = "Saved!"
flash[:alert] = "Problem!"

# Set for CURRENT request (use when rendering, not redirecting)
flash.now[:error] = "Could not save."
render :new, status: :unprocessable_entity

# Carry flash through an extra redirect
flash.keep
redirect_to another_path

目安: redirect_to の前には flash[...] を使います。render の前には flash.now[...] を使います。

手順 8:セッションと Cookie
セッション
# Store
session[:current_user_id] = user.id

# Read
session[:current_user_id]

# Delete
session.delete(:current_user_id)

# Nuke everything (do this on login to prevent session fixation)
reset_session
Cookie
# Basic (deleted when browser closes)
cookies[:theme] = "dark"

# With expiration
cookies[:theme] = { value: "dark", expires: 1.year }

# Permanent (20 years)
cookies.permanent[:locale] = "en"

# Signed (tamper-proof, but readable)
cookies.signed[:user_id] = current_user.id
cookies.signed[:user_id]  # => 42

# Encrypted (tamper-proof AND unreadable)
cookies.encrypted[:token] = "secret"
cookies.encrypted[:token]  # => "secret"

# Delete
cookies.delete(:theme)
手順 9:rescue_from によるエラー処理
class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :not_found
  rescue_from ActiveRecord::RecordInvalid, with: :unprocessable
  rescue_from ActionController::ParameterMissing, with: :bad_request

  private
    def not_found
      respond_to do |format|
        format.html { render file: Rails.root.join("public/404.html"), status: :not_found, layout: false }
        format.json { render json: { error: "Not found" }, status: :not_found }
      end
    end

    def unprocessable(exception)
      respond_to do |format|
        format.html { redirect_back fallback_location: root_path, alert: exception.message }
        format.json { render json: { error: exception.message }, status: :unprocessable_entity }
      end
    end

    def bad_request(exception)
      render json: { error: exception.message }, status: :bad_request
    end
end

Exception や StandardError は rescue しないでください — SystemExit や SyntaxError なども捕捉し、Rails 内部の動作を壊したり、本当のバグを隠したりします。

手順 10:CSRF 保護

デフォルトで有効です。主なポイントは次のとおりです。

# Default — raises exception on CSRF failure
protect_from_forgery with: :exception

# For API controllers — skip CSRF (use token auth instead)
class Api::BaseController < ActionController::API  # no CSRF by default
end

Turbo と rails-ujs は CSRF を自動的に処理します。カスタムの fetch 呼び出しでは、<meta name="csrf-token"> タグからトークンを読み取り、X-CSRF-Token ヘッダーとして送信してください。

手順 11:HTTP 認証、ストリーミング、リクエスト/レスポンス
# Basic Auth — quick and dirty (admin panels, staging)
http_basic_authenticate_with name: "admin", password: ENV["ADMIN_PASSWORD"]

# Token Auth — for APIs
authenticate_or_request_with_http_token do |token, _options|
  ActiveSupport::SecurityUtils.secure_compare(token, ENV["API_TOKEN"])
end

# Send generated data as download
send_data pdf_content, filename: "report.pdf", type: "application/pdf"

# Send existing file
send_file Rails.root.join("storage/report.pdf"), filename: "report.pdf"

# Live streaming — always close the stream
include ActionController::Live
response.headers["Content-Type"] = "text/event-stream"
response.stream.write "data: hello\n\n"
ensure
  response.stream.close

# Useful request properties
request.remote_ip          # client IP
request.get? / request.post?  # method checks
request.headers["X-Custom"]
request.format             # Mime::HTML, Mime::JSON, etc.
request.variant = :mobile  # for device-specific views

避けるべきアンチパターン

  1. 肥大化したコントローラー — ビジネスロジックはモデル/サービス/フォームオブジェクトに移します
  2. permit! — 大量代入攻撃を招くため、許可する属性をすべて列挙します
  3. モデル呼び出しで params[:foo] を直接使うこと — ストロングパラメーターによるフィルタリングを回避してしまうため、params メソッドを経由します
  4. 広範囲に CSRF 対策を無効にすること — トークン認証を使う正真正銘の API エンドポイントに限って無効にします。CSRF 対策は、クロスサイトフォーム送信から保護します
  5. rescue_from Exception — 構文エラーや SystemExit など、握りつぶしたくない例外まで捕捉してしまいます
  6. アクション内のネストされた if/else チェーン — サービスオブジェクトに切り出します
  7. 複数回のレンダリング/リダイレクト — コントローラーアクションでレンダリングまたはリダイレクトできるのは1回だけです。and return または早期リターンを使います
  8. エラー時のレンダリングでステータスを指定しないこと — Turbo では status: :unprocessable_entity が必要です
  9. あらゆる用途でセッションを使うこと — CookieStore のセッションは最大4KBです。大きなデータにはデータベースを使います
  10. :see_other を指定せず、DELETE の後にリダイレクトすること — ブラウザーが DELETE を再実行する可能性があります

クイックリファレンス

詳細なパターンと例については、references/ ディレクトリを参照してください:

  • references/strong-params.md — パラメーター許可パターンの全容(ネストしたハッシュ、配列、二重配列構文)
  • references/callbacks.md — すべてのコールバックの種類、実行順序のルール、停止方法、スキップパターン
  • references/rendering.md — レンダリングオプション、リダイレクト、フラッシュメッセージ、respond_to、バリアントの全容
  • references/sessions-and-cookies.md — セッションストア、Cookie jar の種類、設定
  • references/security.md — rescue_from、CSRF、HTTP 認証、CSP、ログのフィルタリング、Force SSL、ブラウザーバージョン管理
  • references/streaming.md — send_data/send_file、SSE ストリーミング、リクエスト/レスポンスオブジェクト、API コントローラー、ヘルスチェック

コメント