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.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
examples/restful-controllers.rbreferences/routing-patterns.mdreferences/strong-parameters.mdRouting 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.
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.
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:
| Verb | Path | Controller#Action | Purpose |
|---|---|---|---|
| GET | /products | products#index | List all products |
| GET | /products/new | products#new | Form to create product |
| POST | /products | products#create | Create product |
| GET | /products/:id | products#show | Show specific product |
| GET | /products/:id/edit | products#edit | Form to edit product |
| PATCH/PUT | /products/:id | products#update | Update product |
| DELETE | /products/:id | products#destroy | Delete product |
Plus path helpers:
products_path → /productsnew_product_path → /products/newproduct_path(@product) → /products/42edit_product_path(@product) → /products/42/editThis is the Rails Way: conventional routes that cover standard CRUD operations.
For resources that exist once per user (like a profile or settings), use singular routing:
resource :profile # No :id needed
Creates these routes:
| Verb | Path | Controller#Action | Purpose |
|---|---|---|---|
| GET | /profile/new | profiles#new | Form to create profile |
| POST | /profile | profiles#create | Create profile |
| GET | /profile | profiles#show | Show profile |
| GET | /profile/edit | profiles#edit | Form to edit profile |
| PATCH/PUT | /profile | profiles#update | Update profile |
| DELETE | /profile | profiles#destroy | Delete profile |
Notice: no index action, no :id in URLs.
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 1And 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)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)
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
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 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:
ProductsControllerproducts resource@product) available in viewsThe 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"
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.
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 completesaround_action - wraps action executionskip_before_action :callback_name - skip inherited callbacksImplicit 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.
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 keysAccess 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!"
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
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
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
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.
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
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'
Extract complex operations:
# app/services/order_placement_service.rb
class OrderPlacementService
def initialize(order, user)
@order = order
@user = user
end
def call
ActiveRecord::Base.transaction do
assign_user
calculate_totals
charge_payment
send_confirmation
update_inventory
end
rescue => e
@order.errors.add(:base, e.message)
false
end
private
# ... implementation
end
# Controller
def create
@order = Order.new(order_params)
service = OrderPlacementService.new(@order, current_user)
if service.call
redirect_to @order
else
render :new
end
end
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
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 securityreferences/controller-testing.md: Testing controllers effectivelyFor code examples:
examples/restful-controllers.rb: Complete CRUD controller examplesRouting and controllers in Rails are about:
resources helperexpect in Rails 8)Master routing and controllers, and you master how Rails applications respond to the world.