Welcome back to the "Ruby for AI" series. By now you have probably noticed something about Rails: it gets really productive when you stop fighting its defaults. That idea matters even more once you start building AI features.
A lot of people assume AI apps need a giant frontend stack, endless client-side state, and a small forest of JavaScript libraries just to feel modern. Sometimes that is true. Often it is not.
If your app is mostly forms, lists, chat-like interactions, live updates, and server-driven UI, Hotwire and Turbo can get you surprisingly far with much less complexity.
In this post, we will cover what Hotwire is, how Turbo Drive, Turbo Frames, and Turbo Streams work, and why this stack is a great fit for AI builders who want fast iteration without drowning in frontend sprawl.
What Hotwire actually is
Hotwire is the Rails approach to building modern interactive apps while keeping most of the application logic on the server.
The core pieces are:
- Turbo Drive for fast page navigation
- Turbo Frames for partial page updates
- Turbo Streams for live DOM updates from the server
- Stimulus for lightweight JavaScript behavior
In this post, we are focusing on the Turbo side. Stimulus gets its own article next.
The important shift is this: instead of shipping a fat client app that owns everything in the browser, you let the server stay in charge of rendering most UI, and you update only the parts that need changing.
That works extremely well for many AI interfaces.
Why Turbo is a strong fit for AI apps
AI apps often look more complicated than they really are.
Under the hood, a lot of them are just:
- submit prompt
- show status
- render result
- update history
- maybe stream partial output
That is exactly the kind of interaction model Turbo likes.
Examples:
- a prompt playground
- a support ticket summarizer
- an internal content drafting tool
- a document analysis dashboard
- a review queue with AI-generated suggestions
These are not games. They do not need a full frontend operating system. They need responsive UI, fast feedback, and maintainable code.
Turbo gives you that with less ceremony.
Turbo Drive: faster navigation with almost no work
Turbo Drive replaces the old full-page browser navigation flow with a faster one. It intercepts link clicks and form submissions, fetches the next page in the background, and swaps in the new HTML.
For you, that often means you get SPA-like smoothness while still writing normal Rails views.
If you generate a modern Rails app with Hotwire enabled, Turbo Drive is already there.
That means a page like this:
<%= link_to "Prompt Runs", prompt_runs_path %>
<%= link_to "New Prompt", new_prompt_run_path %>
already benefits from faster navigation without you rewriting anything into JSON APIs and client-side routing.
For AI builders, this matters because you can iterate fast. You keep the simple Rails mental model, but the UI still feels snappy.
Turbo Frames: update part of the page, not the whole thing
Turbo Frames are where things get interesting.
A Turbo Frame defines a part of the page that can be updated independently.
Example:
<turbo-frame id="prompt_form">
<%= render "form", prompt_run: @prompt_run %>
</turbo-frame>
Now imagine your form submits and instead of re-rendering the whole page, the server only replaces that frame.
Controller example:
class PromptRunsController < ApplicationController
def new
@prompt_run = PromptRun.new
end
def create
@prompt_run = PromptRun.new(prompt_run_params)
if @prompt_run.save
redirect_to @prompt_run
else
render :new, status: :unprocessable_entity
end
end
private
def prompt_run_params
params.require(:prompt_run).permit(:prompt, :model_name)
end
end
The same Rails response flow still works, but now the user experience is much tighter.
This is useful in AI apps where you want:
- a prompt form to update inline
- a result card to refresh without blowing away the page
- a settings panel to save independently
- a chat sidebar to update while the rest of the app stays put
Turbo Frames example: prompt + result area
Suppose you want a page with a prompt form on the left and the latest AI result on the right.
<div class="layout">
<turbo-frame id="prompt_form">
<%= render "form", prompt_run: @prompt_run %>
</turbo-frame>
<turbo-frame id="latest_result">
<%= render "result", prompt_run: @latest_prompt_run %>
</turbo-frame>
</div>
Now you can target only the result frame when the backend finishes processing or when the user submits a new prompt.
That is a clean server-driven UX without dragging in a big client-side state management setup.
Turbo Streams: live updates from the server
Turbo Streams let the server send instructions like:
- append this HTML
- replace that element
- remove this row
- prepend a new item to a list
That is powerful for AI apps because many interactions are incremental.
For example, when a new prompt run is created, you may want to prepend it to a history list.
In your controller:
def create
@prompt_run = PromptRun.new(prompt_run_params.merge(status: "queued"))
if @prompt_run.save
respond_to do |format|
format.html { redirect_to prompt_runs_path }
format.turbo_stream
end
else
render :new, status: :unprocessable_entity
end
end
Then create a matching stream template:
<!-- app/views/prompt_runs/create.turbo_stream.erb -->
<%= turbo_stream.prepend "prompt_runs", partial: "prompt_runs/prompt_run", locals: { prompt_run: @prompt_run } %>
<%= turbo_stream.replace "prompt_form", partial: "prompt_runs/form", locals: { prompt_run: PromptRun.new } %>
And your list page:
<div id="prompt_runs">
<%= render @prompt_runs %>
</div>
Now when a user submits a prompt, the list updates instantly without a full reload.
That is exactly the kind of thing people often over-engineer with frontend frameworks.
Where Turbo Streams shine in AI workflows
Turbo Streams are especially nice when you have status changes.
Imagine a PromptRun goes through these states:
- queued
- running
- completed
- failed
A background job can update the record, and your app can broadcast UI updates as the state changes.
Example partial:
<!-- app/views/prompt_runs/_prompt_run.html.erb -->
<div id="<%= dom_id(prompt_run) %>">
<p><strong>Prompt:</strong> <%= prompt_run.prompt %></p>
<p><strong>Status:</strong> <%= prompt_run.status %></p>
<p><strong>Output:</strong> <%= prompt_run.output %></p>
</div>
Then from the model:
class PromptRun < ApplicationRecord
broadcasts_to ->(prompt_run) { "prompt_runs" }, inserts_by: :prepend
end
Now your UI can reflect status changes live with far less custom client-side code.
For AI builders, that is huge. Many interfaces are basically job dashboards with pretty boxes. Turbo Streams handles that really well.
What Turbo does not magically solve
Turbo is great, but do not turn it into a religion either.
It is strongest when:
- the server should stay in charge of rendering
- interactions are form- and CRUD-heavy
- real-time updates are DOM-level, not canvas/game-level
- you want to move fast with Rails conventions
It is weaker when:
- the UI is extremely client-heavy
- you need complex drag/drop state everywhere
- you are building something closer to Figma than Basecamp
- you need very custom frontend rendering pipelines
That is fine. The point is not "never use JavaScript." The point is "do not assume you need a giant frontend architecture before the app earns it."
A practical AI example: prompt playground
Imagine you are building a prompt testing interface.
User flow:
- type prompt
- submit form
- see queued state
- see completed output appear
- history updates live
That can be built beautifully with:
- Rails views
- Turbo Frames
- Turbo Streams
- a background job
- minimal JavaScript
That means fewer moving parts, faster iteration, and easier debugging.
When you are experimenting with AI features, that matters more than frontend purity contests.
Common mistakes when using Turbo
1. Forgetting that HTML is the transport
Turbo is not JSON-first. It works by sending HTML fragments and stream actions. That means your partials matter.
2. Overcomplicating frame boundaries
If everything is a frame, the page becomes hard to reason about. Use frames where isolated updates actually help.
3. Fighting Rails redirects
A lot of Turbo pain comes from trying to force weird flows instead of leaning into normal Rails controller patterns.
4. Reaching for a frontend framework too early
If your app is mostly forms, lists, and live status changes, Turbo is probably enough.
Final takeaway
Hotwire and Turbo are a genuinely strong fit for many AI products.
They let you build interfaces that feel modern while keeping the Rails server in control. That means:
- less frontend sprawl
- fewer moving parts
- easier iteration
- faster delivery
And for AI builders, that tradeoff is often exactly right.
A lot of AI products are not frontend masterpieces. They are workflow tools with prompts, jobs, outputs, and history. Turbo handles that world very well.
In the next Ruby for AI post, we will cover Stimulus, which is the perfect companion when you need just enough JavaScript to add polish without turning your app into a JS maintenance project.
Top comments (0)