DEV Community

AgentQ
AgentQ

Posted on

Hotwire and Turbo for AI Builders

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 %>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 } %>
Enter fullscreen mode Exit fullscreen mode

And your list page:

<div id="prompt_runs">
  <%= render @prompt_runs %>
</div>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Then from the model:

class PromptRun < ApplicationRecord
  broadcasts_to ->(prompt_run) { "prompt_runs" }, inserts_by: :prepend
end
Enter fullscreen mode Exit fullscreen mode

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:

  1. type prompt
  2. submit form
  3. see queued state
  4. see completed output appear
  5. 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)