Marketplace

Unnamed Skill

This skill should be used when the user asks about Rails routing, URL patterns, RESTful routes, route helpers, controller actions, strong parameters, before_action callbacks, rendering, redirecting, the params hash, nested resources, route constraints, controller concerns, or request/response handling. Also use when discussing how URLs map to code, route organization, or controller best practices. Examples:

$ Instalar

git clone https://github.com/sjnims/rails-expert /tmp/rails-expert && cp -r /tmp/rails-expert/plugins/rails-expert/skills/routing-controllers ~/.claude/skills/rails-expert

// tip: Run this command in your terminal to install the skill


name: routing-controllers description: This skill should be used when the user asks about Rails routing, URL patterns, RESTful routes, route helpers, controller actions, strong parameters, before_action callbacks, rendering, redirecting, the params hash, nested resources, route constraints, controller concerns, or request/response handling. Also use when discussing how URLs map to code, route organization, or controller best practices. Examples:

Routing & Controllers: Rails Request/Response Cycle

Overview

Routing and controllers form the entry point for all web requests in Rails. The router matches incoming HTTP requests to controller actions, and controllers coordinate the response. Understanding this layer is essential for building Rails applications.

Rails routing is designed around RESTful principles, using HTTP verbs (GET, POST, PATCH, DELETE) combined with URL paths to map to specific controller actions. This convention-based approach eliminates configuration while providing powerful flexibility when needed.

Controllers in Rails are thin coordinators. They parse requests, delegate to models, and render responses. They should NOT contain business logic—that belongs in models. Controllers handle HTTP concerns, nothing more.

The Rails Router

How Routing Works

When a request arrives, Rails asks the router to match it:

Request: GET /products/42
Router: Matches to ProductsController#show with params[:id] = "42"
Rails: Creates ProductsController instance, calls show method
Controller: Fetches product, renders view

The router is defined in config/routes.rb and uses a Ruby DSL to map URLs to controllers.

RESTful Resource Routing

Rails defaults to RESTful routing via the resources helper:

# config/routes.rb
Rails.application.routes.draw do
  resources :products
end

This single line creates 7 routes:

VerbPathController#ActionPurpose
GET/productsproducts#indexList all products
GET/products/newproducts#newForm to create product
POST/productsproducts#createCreate product
GET/products/:idproducts#showShow specific product
GET/products/:id/editproducts#editForm to edit product
PATCH/PUT/products/:idproducts#updateUpdate product
DELETE/products/:idproducts#destroyDelete product

Plus path helpers:

  • products_path/products
  • new_product_path/products/new
  • product_path(@product)/products/42
  • edit_product_path(@product)/products/42/edit

This is the Rails Way: conventional routes that cover standard CRUD operations.

Singular Resources

For resources that exist once per user (like a profile or settings), use singular routing:

resource :profile  # No :id needed

Creates these routes:

VerbPathController#ActionPurpose
GET/profile/newprofiles#newForm to create profile
POST/profileprofiles#createCreate profile
GET/profileprofiles#showShow profile
GET/profile/editprofiles#editForm to edit profile
PATCH/PUT/profileprofiles#updateUpdate profile
DELETE/profileprofiles#destroyDelete profile

Notice: no index action, no :id in URLs.

Nested Resources

When resources have parent-child relationships, nest routes:

resources :categories do
  resources :products
end

Creates URLs like:

  • /categories/1/products → products in category 1
  • /categories/1/products/new → new product in category 1
  • /categories/1/products/5 → product 5 in category 1

And path helpers:

  • category_products_path(@category)
  • new_category_product_path(@category)
  • category_product_path(@category, @product)

Best Practice: Limit nesting to 1 level deep. Beyond that, use shallow nesting:

resources :categories do
  resources :products, shallow: true
end

This creates:

  • /categories/1/products (collection route, needs category)
  • /products/5 (member route, doesn't need category)

Custom Routes

Beyond RESTful defaults, add custom actions:

resources :products do
  member do
    post :duplicate    # /products/:id/duplicate
    get :preview       # /products/:id/preview
  end

  collection do
    get :search        # /products/search
    post :bulk_update  # /products/bulk_update
  end
end

Member routes act on a specific resource (require :id) Collection routes act on the collection (no :id)

Route Constraints

Constrain routes to match only certain patterns:

# Only numeric IDs
resources :products, constraints: { id: /\d+/ }

# Only specific formats
resources :products, constraints: { format: /json|xml/ }

# Complex constraints
constraints(subdomain: 'api') do
  resources :products, defaults: { format: :json }
end

Route Order Matters

Rails matches routes in order. More specific routes should come first:

# WRONG ORDER
resources :photos
get 'photos/search', to: 'photos#search'  # Never matches!

# CORRECT ORDER
get 'photos/search', to: 'photos#search'  # Matches first
resources :photos

See references/routing-patterns.md for advanced routing techniques.

Controllers

Controller Basics

Controllers inherit from ApplicationController and define actions as public methods:

class ProductsController < ApplicationController
  def index
    @products = Product.all
    # Rails automatically renders app/views/products/index.html.erb
  end

  def show
    @product = Product.find(params[:id])
    # Rails automatically renders app/views/products/show.html.erb
  end
end

Rails conventions:

  • Controller name is plural: ProductsController
  • Corresponds to products resource
  • Actions match RESTful route names
  • Instance variables (@product) available in views
  • Views render automatically unless you specify otherwise

The params Hash

The params hash contains all request data:

# URL: /products?category=electronics&page=2
params[:category]  # => "electronics"
params[:page]      # => "2"

# Route: /products/:id
params[:id]        # => "42" (from URL)

# Form submission
params[:product]   # => { "name" => "Widget", "price" => "9.99" }

Important: params values are always strings. Convert as needed:

page = params[:page].to_i
price = params[:price].to_f
published = params[:published] == "true"

Strong Parameters (Rails 8)

Rails 8 introduces params.expect for safer parameter handling:

def create
  # Old way (still works)
  @product = Product.new(product_params)

  # Rails 8 way
  @product = Product.new(params.expect(product: [:name, :price, :description]))

  if @product.save
    redirect_to @product
  else
    render :new
  end
end

private

# Traditional strong parameters
def product_params
  params.require(:product).permit(:name, :price, :description)
end

expect is clearer and more explicit than require(...).permit(...).

Nested parameters:

params.expect(product: [:name, :price, category: [:name, :description]])

Arrays:

params.expect(product: [:name, tags: []])

See references/strong-parameters.md for comprehensive coverage.

Before Actions (Callbacks)

Run code before actions execute:

class ProductsController < ApplicationController
  before_action :set_product, only: [:show, :edit, :update, :destroy]
  before_action :require_login
  before_action :authorize_admin, only: [:edit, :update, :destroy]

  def show
    # @product already set by before_action
  end

  def edit
    # @product set, login required, admin authorized
  end

  private

  def set_product
    @product = Product.find(params[:id])
  end

  def require_login
    redirect_to login_path unless logged_in?
  end

  def authorize_admin
    redirect_to root_path unless current_user.admin?
  end
end

Other callbacks:

  • after_action - runs after action completes
  • around_action - wraps action execution
  • skip_before_action :callback_name - skip inherited callbacks

Rendering and Redirecting

Implicit rendering:

def show
  @product = Product.find(params[:id])
  # Automatically renders app/views/products/show.html.erb
end

Explicit rendering:

def show
  @product = Product.find(params[:id])
  render :show  # Same as implicit
  render 'products/show'  # Full path
  render template: 'products/show'
  render file: '/path/to/template'
end

Rendering different formats:

def show
  @product = Product.find(params[:id])

  respond_to do |format|
    format.html  # Renders show.html.erb
    format.json { render json: @product }
    format.xml  { render xml: @product }
  end
end

Redirecting:

redirect_to @product              # Uses product_path(@product)
redirect_to products_path         # List of products
redirect_to root_path             # Application root
redirect_to 'https://example.com' # External URL
redirect_to products_path, notice: 'Product created!'
redirect_to products_path, alert: 'Error occurred!'

Important: You can only render OR redirect once per action. Doing both causes an error.

The Flash

Temporary messages for the next request:

def create
  @product = Product.new(product_params)
  if @product.save
    flash[:notice] = "Product created successfully!"
    redirect_to @product
  else
    flash.now[:alert] = "Error creating product"
    render :new
  end
end
  • flash[:notice] - persists to next request (for redirects)
  • flash.now[:alert] - only current request (for renders)
  • flash[:success], flash[:error], flash[:warning] - custom keys

Access in views:

<% if flash[:notice] %>
  <div class="notice"><%= flash[:notice] %></div>
<% end %>

Shorthand for redirect:

redirect_to @product, notice: "Created!"
redirect_to @product, alert: "Error!"

Sessions and Cookies

Session:

# Store data across requests
session[:user_id] = @user.id
current_user_id = session[:user_id]
session.delete(:user_id)  # Logout

Cookies:

# Persistent client-side storage
cookies[:theme] = 'dark'
cookies[:theme]  # => "dark"
cookies.delete(:theme)

# Signed cookies (tamper-proof)
cookies.signed[:user_id] = @user.id

# Encrypted cookies (secret)
cookies.encrypted[:api_token] = @user.api_token

Controller Concerns

Extract shared behavior into concerns:

# app/controllers/concerns/authenticable.rb
module Authenticable
  extend ActiveSupport::Concern

  included do
    before_action :require_login
  end

  private

  def require_login
    redirect_to login_path unless logged_in?
  end

  def logged_in?
    !!current_user
  end

  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end
end

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  include Authenticable

  # now has require_login, logged_in?, current_user methods
end

Controller Best Practices

Keep Controllers Thin

Controllers should coordinate, not implement business logic:

Bad (Fat Controller):

def create
  @order = Order.new(order_params)
  @order.user = current_user

  # Business logic in controller - BAD!
  total = 0
  @order.line_items.each do |item|
    total += item.quantity * item.product.price
  end
  @order.total = total

  if @order.total > 1000
    @order.discount = @order.total * 0.1
  end

  # More business logic...
  if @order.save
    # Email logic in controller - BAD!
    OrderMailer.confirmation(@order).deliver_later

    # Inventory logic in controller - BAD!
    @order.line_items.each do |item|
      item.product.decrement!(:inventory, item.quantity)
    end

    redirect_to @order
  else
    render :new
  end
end

Good (Thin Controller, Fat Model):

def create
  @order = Order.new(order_params)
  @order.user = current_user

  if @order.place  # Business logic in model
    redirect_to @order, notice: "Order placed!"
  else
    render :new
  end
end

# app/models/order.rb
class Order < ApplicationRecord
  def place
    transaction do
      calculate_total
      apply_discount
      save!
      send_confirmation
      update_inventory
    end
  rescue ActiveRecord::RecordInvalid
    false
  end

  private

  def calculate_total
    self.total = line_items.sum { |item| item.quantity * item.product.price }
  end

  def apply_discount
    self.discount = total * 0.1 if total > 1000
  end

  def send_confirmation
    OrderMailer.confirmation(self).deliver_later
  end

  def update_inventory
    line_items.each { |item| item.product.decrement!(:inventory, item.quantity) }
  end
end

Standard CRUD Actions

Follow the pattern:

class ProductsController < ApplicationController
  before_action :set_product, only: [:show, :edit, :update, :destroy]

  def index
    @products = Product.all
  end

  def show
  end

  def new
    @product = Product.new
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to @product, notice: 'Created!'
    else
      render :new
    end
  end

  def edit
  end

  def update
    if @product.update(product_params)
      redirect_to @product, notice: 'Updated!'
    else
      render :edit
    end
  end

  def destroy
    @product.destroy
    redirect_to products_path, notice: 'Deleted!'
  end

  private

  def set_product
    @product = Product.find(params[:id])
  end

  def product_params
    params.expect(product: [:name, :price, :description])
  end
end

This pattern is conventional, predictable, and maintainable.

Error Handling

Handle missing records gracefully:

def set_product
  @product = Product.find(params[:id])
rescue ActiveRecord::RecordNotFound
  redirect_to products_path, alert: "Product not found"
end

Or use find_by to avoid exceptions:

def set_product
  @product = Product.find_by(id: params[:id])
  redirect_to products_path, alert: "Product not found" unless @product
end

Request and Response Objects

Access request details:

request.remote_ip        # Client IP
request.format           # Requested format (:html, :json, etc.)
request.method           # HTTP verb (:get, :post, etc.)
request.headers          # HTTP headers
request.path             # URL path
request.fullpath         # Path with query string
request.protocol         # "http://" or "https://"
request.host             # "example.com"
request.port             # 80, 443, etc.

Modify response:

response.headers['X-Custom-Header'] = 'value'
response.status = 404
response.content_type = 'application/json'

Common Patterns

Responders

Handle multiple formats cleanly:

def show
  @product = Product.find(params[:id])

  respond_to do |format|
    format.html
    format.json { render json: @product }
    format.pdf { render pdf: @product }
  end
end

Further Reading

For deeper exploration:

  • references/routing-patterns.md: Advanced routing techniques (constraints, custom routes, route testing)
  • references/strong-parameters.md: Complete guide to parameter handling and security

For code examples:

  • examples/restful-controllers.rb: Complete CRUD controller examples

Summary

Routing and controllers in Rails are about:

  • RESTful conventions that map HTTP to CRUD operations
  • Resourceful routing via resources helper
  • Thin controllers that coordinate, not implement
  • Strong parameters for security (expect in Rails 8)
  • Before actions for DRY code
  • Concerns for shared behavior
  • Fat models, skinny controllers philosophy

Master routing and controllers, and you master how Rails applications respond to the world.