Ruby on Rails + Hotwire
Table of Contents
1. The Overall Flow: Route → Controller → Action → View
2. Routes
3. Controllers and Actions
4. Views
5. Hotwire
1. The Overall Flow: Route → Controller → Action → View
1.1 The Big Picture
A Rails application can be understood through this basic flow:
Browser
│
│ HTTP request
│ e.g. GET /users
↓
config/routes.rb
│
│ GET /users → UsersController#index
↓
UsersController
│
│ index action
↓
app/views/users/index.html.erb
│
│ ERB generates HTML
↓
HTML sent to the browserThe important roles are:
Route
↓
decides which controller action handles the request
Controller
↓
receives the request and coordinates what should happen
Action
↓
a method inside the controller that handles a particular request
View
↓
generates the HTML that the browser receivesA second relationship is important for understanding views:
config/routes.rb
│
┌──────────────┴──────────────┐
↓ ↓
Routes / URL mapping Route helper methods
↓ ↓
Controller#Action ERB
↓ ↓
View Generated HTMLSo:
routes.rbconnects HTTP requests to controller actions.
And:
Rails conventions connect controller actions to views.
Route helpers are generated from the routes and are commonly used inside ERB templates to generate URLs in the resulting HTML.
1.2 URL vs. Route
A URL path and a route are not the same thing.
For example:
/usersis a URL path.
It does not specify an HTTP method.
These are two different HTTP requests:
GET /users
POST /usersThey have the same URL path, /users, but they are different routes because they use different HTTP methods and map to different controller actions.
A useful definition is:
Route
= HTTP method + URL pattern → Controller#ActionFor example:
GET /users
↓
UsersController#indexThis mapping is defined in config/routes.rb.
Therefore:
URL:
/users
Route:
GET /users → UsersController#indexThe URL is the address. The route is the rule that tells Rails what to do when a request arrives at that address using a particular HTTP method.
1.3 Controller and Action
A controller groups related request-handling logic.
For example:
class UsersController < ApplicationController
endis a controller for users.
An action is a method inside that controller:
class UsersController < ApplicationController
def index
end
def show
end
endHere:
UsersController
├── index action
└── show actionA route can point to a particular action:
GET /users
↓
UsersController#indexThe # notation means:
Controller#ActionSo:
UsersController#indexmeans:
the
indexaction ofUsersController.
1.4 How a Request Becomes HTML
Consider:
GET /usersRails processes it like this:
GET /users
↓
routes.rb
↓
UsersController#index
↓
app/views/users/index.html.erb
↓
HTML
↓
BrowserThe route does not directly point to the ERB file.
The route points to:
UsersController#indexThen Rails follows its conventions to determine which view to render.
2. Routes
2.1 config/routes.rb
Rails routes are normally defined in:
config/routes.rbFor example:
Rails.application.routes.draw do
resources :users
endRails.application.routes.draw do ... end is a Ruby block used to define the application's routes.
resources :users is a Rails routing DSL command that generates the conventional routes for the User resource.
2.2 resources :users
Instead of manually writing seven routes, Rails provides:
resources :usersThis generates seven conventional routes:
index
show
new
create
edit
update
destroyThese names are conventional Rails terminology.
The complete mapping is:
The action name and the route are different concepts.
For example:
indexis the conventional controller action name.
The corresponding route is:
GET /users → UsersController#index2.3 The Seven Conventional Routes
The easiest way to understand the seven routes is to read them as instructions.
INDEX
GET /usersMeaning:
“Show me all users.”
It normally displays a list of users.
GET /users
↓
UsersController#index
↓
app/views/users/index.html.erbWhy index?
In programming and traditional lists/tables, an index is a listing or organized collection of items.
SHOW
GET /users/123Meaning:
“Show me User #123.”
The 123 is the user's ID.
GET /users/123
↓
UsersController#showshow means that Rails should display one particular resource.
NEW
GET /users/newMeaning:
“Give me a form to create a new user.”
This does not create a user.
It only displays the form.
GET /users/new
↓
UsersController#new
↓
app/views/users/new.html.erbThe fact that new uses GET is important.
GET is appropriate because the browser is simply asking:
“Give me the page containing the form.”
No database record needs to be created.
CREATE
POST /usersMeaning:
“Create this new user.”
This is the request that actually attempts to create the database record.
A typical flow is:
User fills out new-user form
↓
POST /users
↓
UsersController#create
↓
User.new(...)
↓
@user.save
↓
databaseNotice that new and create are different:
GET /users/new
= display the form
POST /users
= process the submitted form and create the recordEDIT
GET /users/123/editMeaning:
“Give me a form to edit User #123.”
Again, this does not modify the database.
It displays an editing form:
GET /users/123/edit
↓
UsersController#edit
↓
app/views/users/edit.html.erbUPDATE
PATCH /users/123Meaning:
“Update User #123 with this data.”
This is the request that actually attempts to modify the existing record.
So:
GET /users/123/edit
= display the edit form
PATCH /users/123
= submit changes and update the recordDESTROY
DELETE /users/123Meaning:
“Delete User #123.”
This normally leads to:
UsersController#destroywhere the controller calls the model to delete the record.
2.4 Understanding index, show, new, create, edit, update, and destroy
The seven actions can be memorized as:
index
"Show me the collection."
show
"Show me one existing resource."
new
"Give me a form for creating a new resource."
create
"Create the resource using this submitted data."
edit
"Give me a form for editing an existing resource."
update
"Update the existing resource using this submitted data."
destroy
"Delete the existing resource."The most important pairs are:
new → show a creation form
create → actually create
edit → show an editing form
update → actually updateThe form-displaying actions use GET because they are retrieving a page.
The actions that modify data use methods such as POST, PATCH, and DELETE.
2.5 Route Helper Methods
When Rails creates routes, it also generates route helper methods.
For example:
users_pathreturns:
/usersand:
user_path(@user)might return:
/users/123Other examples:
new_user_path
# => /users/new
edit_user_path(@user)
# => /users/123/editThese helper methods are commonly used in views, especially ERB templates.
For example:
<%= link_to "Create a new user", new_user_path %>Rails generates HTML equivalent to:
<a href="/users/new">Create a new user</a>The helper itself is Ruby code:
new_user_pathThe result is a URL:
/users/newThe URL then becomes part of the generated HTML:
<a href="/users/new">This gives us:
Route helper
↓
URL
↓
HTML_path generates a relative path.
Rails also provides _url variants, such as:
users_urlwhich generates a full URL, such as:
https://example.com/users2.6 collection and member
You can add custom routes to resources.
collection
resources :users do
collection do
get :search
end
endThis means:
Add a route related to the collection of users.
Rails creates:
GET /users/search
↓
UsersController#searchand a helper:
search_users_pathwhich produces:
/users/searchThere is no :id because the action applies to the collection rather than one particular user.
Think:
collection
= the group as a wholemember
resources :users do
member do
get :profile
end
endThis means:
Add a route related to one specific user.
Rails creates:
GET /users/:id/profile
↓
UsersController#profileFor example:
/users/123/profileThe helper is:
profile_user_path(@user)Think:
member
= one specific resourceThe distinction is:
collection
GET /users/search
↓
the collection of users
member
GET /users/123/profile
↓
one specific user2.7 path:, only:, and except:
path:
You can change the URL path while keeping the resource name and helper naming based on users.
resources :users, path: "members"This produces URLs such as:
/members
/members/123
/members/newbut the helpers remain based on users, such as:
users_path
user_path(@user)So the resource name and URL path do not have to be identical.
only:
Generate only the routes you need:
resources :users, only: [:index, :show]This generates only:
GET /users
GET /users/:idexcept:
Generate the resource routes except for specified actions:
resources :users, except: [:destroy]This creates the standard routes other than destroy.
3. Controllers and Actions
3.1 What Is a Controller?
A controller handles HTTP requests and coordinates what the application should do.
For example:
class UsersController < ApplicationController
endA controller commonly:
- receives information from the request,
- calls the model when data needs to be read or changed
- chooses what response to return,
- renders a view or redirects the browser.
3.2 What Is an Action?
An action is a method inside a controller.
class UsersController < ApplicationController
def index
end
def show
end
endHere:
index
showare actions.
A route connects an incoming request to one of these actions:
GET /users
↓
UsersController#index3.3 The Seven Conventional Controller Actions
A conventional UsersController might contain:
class UsersController < ApplicationController
def index
end
def show
end
def new
end
def create
end
def edit
end
def update
end
def destroy
end
endThese action names correspond to the seven routes generated by:
resources :usersThe names are conventions, not arbitrary requirements.
Rails uses these conventions to make the relationship between routes, controllers, and views predictable.
3.4 How Controller Actions Connect to Views
Suppose we have:
def index
endinside:
UsersControllerRails conventionally looks for:
app/views/users/index.html.erbSimilarly:
UsersController#show
↓
app/views/users/show.html.erb
UsersController#new
↓
app/views/users/new.html.erb
UsersController#edit
↓
app/views/users/edit.html.erbThe general convention is:
Controller:
UsersController
↓
View directory:
app/views/users/
↓
Action:
index
↓
View:
index.html.erbThis is why Rails knows to use index.html.erb.
The route does not directly say:
"render index.html.erb"Instead:
GET /users
↓
UsersController#index
↓
Rails convention
↓
app/views/users/index.html.erbThis is an important Rails convention.
3.5 new vs. create
Consider:
def new
@user = User.new
endUser.new creates a Ruby object in memory.
It does not save a database record.
The purpose of new is usually to prepare an object for the form:
GET /users/new
↓
UsersController#new
↓
@user = User.new
↓
new.html.erb
↓
formThen the user submits the form:
POST /users
↓
UsersController#createA typical create action is:
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render :new
end
endLine by line:
@user = User.new(user_params)
User.new(...)creates a User object in memory.
It does not save it yet.
user_params supplies the permitted attributes.
if @user.save
@user.saveasks Active Record to save the object to the database.
It returns:
trueif the save succeeds, or:
falseif it fails, for example because model validations failed.
redirect_to @user
After successful creation:
redirect_to @usertells the browser to navigate to that user’s page.
For a user with ID 123, this is approximately:
redirect_to user_path(@user)which results in:
/users/123The browser then makes another request:
GET /users/123
↓
UsersController#showTherefore:
redirect_tois HTTP/navigation behavior. It is not a database operation.
render :new
If saving fails:
render :newrenders:
app/views/users/new.html.erbin the current request.
Get Natsuko’s stories in your inbox
Join Medium for free to get updates from this writer.
It does not make another HTTP request.
The distinction is:
render
= render a view now
redirect_to
= tell the browser to make another request3.6 edit vs. update
A typical edit action might be:
def edit
@user = User.find(params[:id])
endIts purpose is to retrieve the existing user and display an editing form.
The actual modification happens later:
GET /users/123/edit
↓
UsersController#edit
↓
edit.html.erb
↓
user submits changes
↓
PATCH /users/123
↓
UsersController#updateA typical update action is:
def update
@user = User.find(params[:id])
if @user.update(user_params)
redirect_to @user
else
render :edit
end
endparams[:id]
For:
/users/123Rails provides:
params[:id]with the value:
123User.find(params[:id])
This asks Active Record to find the existing database record.
@user.update(user_params)
This updates the existing record using the permitted parameters and attempts to save the changes to the database.
redirect_to @user
After success, the browser is redirected to the user’s show page.
render :edit
If validation fails, Rails renders the edit form again in the current request.
3.7 render vs. redirect_to
The distinction is fundamental:
rendermeans:
Render a view as the response to the current request.
redirect_tomeans:
Send a redirect response to the browser and tell it to make another request.
For example:
render :newdoes not cause:
GET /users/newto happen again.
But:
redirect_to @usercauses the browser to make another request, typically:
GET /users/1233.8 Database Operations and Active Record
In normal Rails code, the controller coordinates database operations, while the model/Active Record layer performs the actual persistence work.
For example:
@user = User.find(params[:id])asks the User model/Active Record to retrieve a record.
@user.saveasks Active Record to save the object.
@user.update(user_params)asks Active Record to update and save the record.
The controller is responsible for the request flow:
HTTP request
↓
Controller
↓
Model / Active Record
↓
DatabaseThe controller normally does not manually write SQL.
3.9 Strong Parameters: params, require, and permit
A common controller method is:
def user_params
params.require(:user).permit(:name, :email)
endThis is normally defined in the controller, not the model.
For example:
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render :new
end
end
private
def user_params
params.require(:user).permit(:name, :email)
end
endWhat is params?
params contains parameters supplied with the HTTP request.
Conceptually, a submitted form might produce:
{
user: {
name: "Alice",
email: "alice@example.com"
}
}Then:
paramscontains that data.
What does require(:user) do?
params.require(:user)means:
Require a
userparameter to be present, and return that part of the parameters.
Conceptually:
paramscontains:
{
user: {
name: "Alice",
email: "alice@example.com"
}
}Then:
params.require(:user)selects:
{
name: "Alice",
email: "alice@example.com"
}If the user parameter is missing, Rails raises an error.
What does permit(:name, :email) do?
permit(:name, :email)means:
Only allow
nameand
For example, suppose someone submits:
{
user: {
name: "Alice",
email: "alice@example.com",
admin: true
}
}With:
params.require(:user).permit(:name, :email)the permitted parameters contain only:
{
name: "Alice",
email: "alice@example.com"
}admin is not permitted.
This is the important security property of strong parameters.
The application explicitly defines which incoming attributes are allowed to reach the model.
The flow is:
HTTP request
↓
params
↓
require(:user)
↓
select the user parameter
↓
permit(:name, :email)
↓
allow only specified attributes
↓
User.new(user_params)Without this filtering, blindly passing user-controlled parameters into a model could allow attributes that the application did not intend to be changed through that request.
The key line is:
permit(:name, :email)because it creates the allowlist of permitted attributes.
4. Views
4.1 What Is a View?
A view is responsible for presenting application data as HTML.
Rails views are normally stored under:
app/views/For UsersController, the conventional directory is:
app/views/users/For example:
app/views/users/index.html.erb
app/views/users/show.html.erb
app/views/users/new.html.erb
app/views/users/edit.html.erb4.2 ERB
ERB stands for Embedded Ruby.
An .html.erb file is primarily HTML, but it can contain Ruby code.
For example:
<h1>Users</h1><% @users.each do |user| %>
<p><%= user.name %></p>
<% end %>The Ruby code runs on the server and produces HTML.
Two important ERB forms are:
<%= ... %>and:
<% ... %><%= ... %> evaluates Ruby and outputs the result into the HTML.
<% ... %> evaluates Ruby without directly outputting its return value.
4.3 How Rails Finds index.html.erb
Suppose the request is:
GET /usersThe route maps it to:
UsersController#indexIf the action does not explicitly render another view, Rails follows its convention and looks for:
app/views/users/index.html.erbThe mapping is:
UsersController
↓
app/views/users/
index action
↓
index.html.erb
show action
↓
show.html.erb
new action
↓
new.html.erb
edit action
↓
edit.html.erbThis is convention over configuration.
You do not normally need to write:
render "app/views/users/index.html.erb"inside every index action.
Rails can infer the view from the controller and action names.
4.4 Using Route Helpers in ERB
Route helpers are particularly useful in views.
For example:
<%= link_to "Users", users_path %>Here:
users_pathis a Ruby method generated by Rails from the routes.
It returns:
/usersThen link_to uses that URL to generate HTML:
<a href="/users">Users</a>Another example:
<%= link_to "Edit", edit_user_path(@user) %>For User #123, the helper returns:
/users/123/editand Rails generates a link to that URL.
The important distinction is:
users_path
= Ruby helper method
/users
= URL
<a href="/users">
= HTML4.5 From ERB to HTML
A simplified server-side flow is:
Controller provides data
↓
ERB template uses the data
↓
Rails evaluates the Ruby
↓
HTML is generated
↓
Browser receives HTMLFor example:
<h1><%= @user.name %></h1>If:
@user.nameis "Alice", the generated HTML is approximately:
<h1>Alice</h1>This is why ERB is useful: it allows server-side Ruby to generate dynamic HTML.
5. Hotwire
5.1 What Is Hotwire?
Hotwire is a web development approach associated with 37signals and Rails.
It consists primarily of:
Turbo
StimulusThe basic idea is to make web applications feel more dynamic while continuing to work heavily with HTML and server-side Rails.
5.2 Turbo
Turbo focuses on navigation and HTML updates.
Its major pieces are:
Turbo Drive
Turbo Frames
Turbo StreamsThey solve different problems:
Turbo Drive
→ page-level navigation
Turbo Frames
→ navigation/update within a specific region
Turbo Streams
→ specific DOM operations5.3 Stimulus
Stimulus is a JavaScript framework designed around attaching behavior to HTML.
A Stimulus controller can respond to browser events such as:
click
input
submit
changeFor example, JavaScript behavior can be attached to an HTML element using Stimulus attributes.
The conceptual distinction is:
Turbo
= navigation and HTML updates
Stimulus
= JavaScript behavior and interactionTurbo can often handle an interaction without writing custom JavaScript.
Stimulus is useful when the browser needs additional client-side behavior.
5.4 Turbo Drive
Turbo Drive handles normal page navigation.
Consider:
<%= link_to "Users", users_path %>The link still looks like an ordinary Rails link.
Turbo Drive intercepts the navigation, requests the new HTML, and updates the page without requiring the browser to perform a traditional full-page reload.
Conceptually:
User clicks link
↓
GET /users
↓
Server returns HTML
↓
Turbo Drive updates the current documentThe application still uses normal Rails routes and controllers.
You generally do not need to write a special Turbo Drive tag for every link.
Turbo Drive is enabled by default when Turbo is installed.
It can be disabled for a particular element with:
data-turbo="false"5.5 Turbo Frames
Turbo Frames allow a specific region of a page to be updated independently.
For example:
<%= turbo_frame_tag "user_2" do %>
<p>Bob</p>
<%= link_to "Edit", edit_user_path(2) %>
<% end %>This generates an element similar to:
<turbo-frame id="user_2">
<p>Bob</p>
<a href="/users/2/edit">Edit</a>
</turbo-frame>user_2 is simply the HTML element's ID.
It is not a special Rails keyword.
When navigation occurs inside a frame, Turbo can use the response to update that frame rather than replacing the entire page.
The mental model is:
Whole page
┌──────────────────────────────┐
│ Header │
│ │
│ ┌──────────────────────────┐ │
│ │ turbo-frame │ │
│ │ │ │
│ │ User #2 │ │
│ │ Edit │ │
│ │ │ │
│ └──────────────────────────┘ │
│ │
│ Footer │
└──────────────────────────────┘The frame defines a region that Turbo can navigate within.
5.6 Turbo Streams
Turbo Streams are different from Frames.
A Turbo Frame defines a region.
A Turbo Stream gives Turbo an operation to perform on the DOM.
For example:
<turbo-stream action="remove" target="user_2">
</turbo-stream>means:
Find the element with id="user_2"
↓
Remove it from the DOMOther common stream actions include:
append
prepend
replace
update
remove
before
afterFor example:
<turbo-stream action="append" target="comments">
<template>
<p>Hello</p>
</template>
</turbo-stream>means:
Find #comments
↓
Append the supplied HTML inside itA Turbo Stream is therefore an instruction.
It is not itself a button or event listener.
5.7 What Triggers a Turbo Stream?
A Turbo Stream does not spontaneously execute merely because you wrote the markup somewhere.
Something has to cause Turbo to receive and process the stream.
A common example is a form submission.
Suppose a page contains:
<%= button_to "Delete",
user_path(@user),
method: :delete %>The user clicks the button.
The flow is:
User clicks "Delete"
↓
DELETE /users/2
↓
UsersController#destroy
↓
@user.destroy
↓
Server returns a Turbo Stream response
↓
Turbo processes the stream
↓
Target element is removedThe server could return:
<%= turbo_stream.remove dom_id(@user) %>which produces a Turbo Stream instruction similar to:
<turbo-stream action="remove" target="user_2">
</turbo-stream>Turbo then performs the instructed DOM operation.
The button is therefore the trigger for the HTTP request.
The Turbo Stream is the instruction in the response.
These are different things.
append works the same way
Consider:
<turbo-stream action="append" target="comments">
<template>
<p>Hello</p>
</template>
</turbo-stream>This does not contain a button.
It does not need to.
A common flow is:
User submits "Add comment" form
↓
POST /comments
↓
CommentsController#create
↓
Comment is saved
↓
Server returns Turbo Stream
↓
Turbo processes:
action="append"
target="comments"
↓
New comment is appendedSo:
Button / form / link / browser event
↓
causes a request or other interaction
↓
server or client produces a Turbo Stream
↓
Turbo processes the stream instruction
↓
DOM changesThe <turbo-stream> element itself is not the trigger.
5.8 Turbo Drive vs. Turbo Frames vs. Turbo Streams
The three can be remembered with these three questions:
Turbo Drive
“Can I navigate to another page without a traditional full-page reload?”
Link
↓
new page HTML
↓
Turbo updates the documentTurbo Frames
“Can I navigate or update only this specific region?”
Page
↓
specific <turbo-frame>
↓
only that region changesTurbo Streams
“What specific DOM operation should Turbo perform?”
Turbo Stream
↓
append / prepend / replace / update / remove / before / afterThe distinction is:
Turbo Drive
= page-level navigationTurbo Frame
= defined regionTurbo Stream
= DOM operation/instruction
Frames and Streams can also be used together.
For example, a form inside a Turbo Frame can submit to the server, and the server can return a Turbo Stream that updates another part of the page.
Final Example: Putting Everything Together
Consider this route configuration:
Rails.application.routes.draw do
resources :users do
collection do
get :search
end
end
endRails creates the seven standard resource routes plus the custom collection route.
The seven standard routes
ActionHTTP methodURL pathController actionHelperindexGET/usersUsersController#indexusers_pathshowGET/users/:idUsersController#showuser_path(@user)newGET/users/newUsersController#newnew_user_pathcreatePOST/usersUsersController#createusers_patheditGET/users/:id/editUsersController#editedit_user_path(@user)updatePATCH/PUT/users/:idUsersController#updateuser_path(@user)destroyDELETE/users/:idUsersController#destroyuser_path(@user)
And the additional collection route is:
ActionHTTP methodURL pathController actionHelpersearchGET/users/searchUsersController#searchsearch_users_path
The complete conceptual flow is:
config/routes.rb
│
┌─────────────────┴─────────────────┐
│ │
↓ ↓
HTTP request Route helpers
GET /users users_path
│ │
↓ ↓
UsersController#index "/users"
│ │
↓ ↓
app/views/users/index.html.erb ERB template
│ │
└──────────────┬────────────────────┘
↓
Generated HTML
↓
BrowserAnd when Hotwire is involved:
Browser
│
│ User interaction
↓
HTTP request
│
↓
routes.rb
│
↓
Controller#Action
│
├── Model / Active Record
│ ↓
│ Database
│
↓
HTML / Turbo response
│
├── Turbo Drive
│ → page-level navigation
│
├── Turbo Frame
│ → update a specific region
│
└── Turbo Stream
→ perform a specific DOM operation
│
↓
Browser DOMThe core Rails concepts can therefore be summarized as:
URL
= address such as /users/123Route
= HTTP method + URL pattern → Controller#ActionController
= class responsible for handling requestsAction
= method inside a controllerView
= HTML-generating templateERB
= HTML with embedded RubyRoute helper
= Ruby method that generates a URLModel / Active Record
= application layer that handles data and database persistenceTurbo Drive
= page-level navigationTurbo Frame
= a defined region of the pageTurbo Stream
= an instruction for modifying the DOM
And the most important Rails flow to remember is:
HTTP request
↓
Route
↓
Controller#Action
↓
Model / Active Record (when data is needed)
↓
View
↓
HTML
↓
Browserwith Rails conventions making the controller-to-view connection predictable:
UsersController#index
↓
app/views/users/index.html.erband route helpers providing a convenient way for those views to generate URLs:
users_path
↓
/users
↓
<a href="/users">...</a>






