Agency

Replacing SPAs with Rails 7 and Hotwire

Single-page applications changed how teams build the web, but they also introduced a layer of complexity that many products do not actually need. Rails 7 with Hotwire offers another path.

LAST UPDATED: July 13, 2025
9 min read
Replacing SPAs with Rails 7 and Hotwire

Single-page applications changed how teams build the web, but they also introduced a layer of complexity that many products do not actually need. A separate frontend application, API contracts, client-side routing, state management, loading states, hydration, and increasingly large JavaScript bundles can turn ordinary business workflows into distributed systems. Rails 7 with Hotwire offers another path: keep the application server-driven, render real HTML, and use Turbo and Stimulus to make interactions feel fast and modern. The result is not a return to old-fashioned websites. It is a deliberate architecture where the server owns business logic and UI state while the browser receives only the JavaScript it genuinely needs.

Why Teams Are Reconsidering the SPA

The classic SPA architecture looks something like this:

Browser
   ↓
React / Vue / Angular
   ↓
Client State
   ↓
API
   ↓
Backend
   ↓
Database

It is a powerful model.

But every additional boundary introduces another thing the team must design and maintain.

A relatively simple feature can involve:

UI Component
     ↓
Client State
     ↓
API Request
     ↓
Controller
     ↓
Serializer
     ↓
Business Logic
     ↓
Database

Then the frontend may also need to handle:

Routing

Loading states

Error states

Authentication state

Caching

Optimistic updates

Form validation

Data synchronization

None of these are inherently bad.

The question is whether the application actually benefits from all of them.

For many SaaS products, admin platforms, marketplaces, internal tools, and CRUD-heavy applications, the answer may be not always.

What Rails 7 and Hotwire Bring to the Table

Rails has always been particularly good at building server-driven applications.

Rails 7 modernizes that approach through Hotwire.

Instead of:

Browser
   ↓
JavaScript Application
   ↓
API
   ↓
Server

the architecture can become:

Browser
   ↓
Turbo + Stimulus
   ↓
Rails
   ↓
Database

Hotwire consists primarily of:

Turbo Drive

Turbo Frames

Turbo Streams

Stimulus

Together, they allow Rails applications to provide highly interactive experiences without turning the entire browser into a separate application runtime.

The philosophy is simple:

The goal is to send HTML instead of making the browser reconstruct the interface from JSON whenever possible.

SPA vs. Server-Driven Architecture

Consider a simple form.

SPA approach

User
 ↓
React Form
 ↓
API Request
 ↓
JSON Response
 ↓
Client State
 ↓
React Render

Rails + Hotwire approach

User
 ↓
HTML Form
 ↓
Rails
 ↓
Validation / Database
 ↓
HTML Response
 ↓
Turbo Update

The second approach removes several layers.

The server already understands:

The business rules

The user's permissions

The database

The validation

The page structure

Why make the browser reconstruct information the server already knows?

For many applications, letting Rails remain the central application runtime can be considerably easier to reason about.

Turbo Drive: Navigation Without a Client Router

Turbo Drive intercepts standard navigation and can update pages without requiring a complete browser reload for typical application navigation.

The experience becomes:

Click Link
    ↓
Turbo Request
    ↓
Rails
    ↓
HTML
    ↓
Page Update

From the user's perspective, navigation can feel much closer to a SPA.

But the application does not need to maintain a separate client-side router.

The URL remains a real URL.

Rails remains responsible for routing.

That gives teams a useful combination:

Fast navigation

Progressive enhancement

Normal web URLs

Server-side routing

Less client-side infrastructure

The browser feels fast without having to own the entire application.

Turbo Frames: Partial Page Updates

Turbo Frames allow a page to be divided into independently replaceable sections.

Imagine an account page:

Account
│
├── Profile
├── Subscription
├── Billing
└── Notifications

If a user updates their billing details, there is no reason to reload every section.

A Turbo Frame can isolate the relevant area:

Billing Frame
     ↓
User Action
     ↓
Rails
     ↓
Updated HTML
     ↓
Replace Frame

This provides one of the most important Hotwire ideas:

A server-rendered application does not have to mean a full-page refresh for every interaction.

You can preserve server ownership while updating only the relevant part of the interface.

Turbo Streams: Server-Driven UI Changes

Turbo Streams go one step further.

They allow the server to describe targeted DOM changes.

For example, when a user creates a new comment:

Submit Comment
      ↓
Rails
      ↓
Turbo Stream
      ↓
Insert Comment

The server can perform operations such as:

Append

Prepend

Replace

Remove

Update

This becomes especially useful for workflows such as:

Notifications

Chat

Order updates

Activity feeds

Admin dashboards

Background job status

The architecture can remain server-driven while the UI still reacts immediately to changes.

Real-Time Experiences Without a Huge Client State Layer

Consider an operations dashboard.

A new order arrives:

New Order
   ↓
Rails
   ↓
Broadcast
   ↓
Turbo Stream
   ↓
Update Order List

The browser does not necessarily need a global client-side store containing the entire state of the dashboard.

The server can remain the source of truth.

That can eliminate a whole class of synchronization problems.

Instead of asking:

"Is the client state still consistent with the server?"

the architecture can lean toward:

"The server owns the state; the browser displays the current state."

For business applications, that can be a very attractive model.

Stimulus: JavaScript Where It Actually Belongs

Hotwire does not mean eliminating JavaScript.

It means using JavaScript selectively.

Stimulus is designed for focused client-side behavior such as:

Dropdowns

Modals

Tabs

Autocomplete

Clipboard actions

Drag-and-drop

Client-side UI state

For example:

Rails HTML
    ↓
Stimulus Controller
    ↓
Specific Interaction

Instead of:

Entire Application
       ↓
Large JavaScript Runtime
       ↓
Everything Runs Client-Side

This creates a useful rule:

If an interaction genuinely requires the browser, use JavaScript. If it does not, let the server handle it.

That distinction keeps client-side code focused.

Forms Without an API Layer

Forms are where this architecture can become particularly compelling.

In a traditional SPA:

Form
 ↓
JavaScript
 ↓
API
 ↓
JSON
 ↓
Client State
 ↓
UI Update

With Rails and Hotwire:

Form
 ↓
Rails Controller
 ↓
Validation
 ↓
Database
 ↓
HTML / Turbo Response
 ↓
UI Update

Rails already provides mechanisms for:

Validation

Authentication

Authorization

Persistence

Error handling

Redirects

CSRF protection

There may be little value in creating a separate API endpoint just to submit a form that is ultimately consumed by the same Rails application.

This can dramatically reduce duplicated frontend/backend logic.

Authentication and Authorization

A server-driven architecture can also simplify security boundaries.

Instead of:

Browser
 ↓
Token
 ↓
API
 ↓
Authorization

a Rails application can use its established server-side session and authorization mechanisms:

Browser
 ↓
Rails Session
 ↓
Authorization
 ↓
Controller / Policy
 ↓
HTML

This can reduce the amount of authentication state that needs to live in client-side JavaScript.

The result is a simpler mental model:

The server knows who the user is and what they are allowed to do.

That does not eliminate security work, but it can reduce the number of places where authentication logic has to be implemented.

Performance and User Experience

One of the strongest arguments for reducing SPA complexity is the cost of client-side JavaScript.

A large SPA may require the browser to:

Download
   ↓
Parse
   ↓
Compile
   ↓
Execute
   ↓
Initialize
   ↓
Fetch Data
   ↓
Render

A server-driven application can move much of that work to the server:

Request
   ↓
Rails
   ↓
HTML
   ↓
Browser

Then JavaScript is introduced only where interaction requires it.

This can be particularly useful for:

Mobile devices

Slower networks

Content-heavy applications

Business software

Admin interfaces

E-commerce workflows

But Rails + Hotwire is not magically fast.

Slow database queries, excessive HTML, poor caching, large assets, and inefficient server-side rendering can still produce a slow application.

The architecture simply gives the team a different set of performance trade-offs.

When a SPA Is Still the Better Choice

Replacing SPAs should never become an ideological exercise.

Some products genuinely benefit from a rich client architecture.

Examples include:

Browser-based design tools

Online IDEs

Complex editors

Graphics-heavy applications

Real-time collaboration tools

Offline-first applications

Highly interactive data visualization

In these products, the browser may need to maintain substantial state locally.

The architecture might legitimately look like:

Browser
   ↓
Rich Client Runtime
   ↓
Local State
   ↓
Continuous Interaction

That is not a problem.

The important question is:

Does the product need this level of client-side complexity?

If yes, use it.

If not, there may be a simpler alternative.

Common Migration Mistakes

Rebuilding the SPA Inside Rails

If every feature still requires extensive JavaScript state and API calls, the migration may simply move complexity around.

Creating APIs for Every Interaction

Not every form, button, or page needs a JSON endpoint.

Use server-rendered HTML where it makes sense.

Turning Stimulus Into Another SPA Framework

Stimulus should provide focused behavior.

If it becomes a massive client-side application with its own state architecture, you may be recreating the problem you're trying to solve.

Ignoring Server Performance

Moving work to Rails means the server becomes more important.

Optimize:

Database queries

Caching

Rendering

Background jobs

Server response time

Migrating Everything at Once

A complete rewrite introduces unnecessary risk.

Incremental migration is usually easier to validate.

Removing JavaScript Too Aggressively

Some interactions genuinely belong in the browser.

The goal is not zero JavaScript.

The goal is purposeful JavaScript.

A Modern Rails + Hotwire Architecture

A production application can remain surprisingly compact:

                         Browser
                            │
                ┌───────────┴───────────┐
                ▼                       ▼
             Turbo                   Stimulus
                │                       │
                └───────────┬───────────┘
                            ▼
                          Rails
                            │
             ┌──────────────┼──────────────┐
             ▼              ▼              ▼
        Controllers      Services        Models
             │              │              │
             └──────────────┼──────────────┘
                            ▼
                         Database

For live updates:

Rails
  ↓
Turbo Streams
  ↓
Broadcast
  ↓
Browser

This keeps the architecture cohesive.

Instead of maintaining:

Frontend Repository
+
Backend Repository
+
API Contract
+
Client State
+
Deployment Coordination

a team can often keep much of the application in one place.

How to Migrate an Existing SPA

A migration does not need to start with a rewrite.

Step 1 — Find Server-Friendly Screens

Start with pages dominated by:

Forms

Tables

CRUD

Content

Business workflows

These are often excellent candidates.

Step 2 — Move Routing to Rails

Allow Rails to own routes that do not require complex client-side navigation.

Step 3 — Replace API-Driven Forms

Use Rails forms and Turbo responses.

Step 4 — Introduce Turbo Frames

Identify page sections that need independent updates.

Step 5 — Introduce Turbo Streams

Use them for targeted updates and real-time workflows.

Step 6 — Extract Genuine Browser Behavior

Move only necessary interactions into Stimulus controllers.

Step 7 — Remove Unnecessary Client State

If the server already owns the authoritative state, do not duplicate it in the browser without a reason.

Step 8 — Measure Everything

Compare the old and new architectures rather than relying on assumptions.

Measuring the Migration

A successful migration should produce measurable improvements.

Frontend Complexity

Track:

JavaScript bundle size

Number of client components

Client-side state stores

API endpoints

Performance

Measure:

Largest Contentful Paint

Interaction to Next Paint

Time to First Byte

Page navigation time

Engineering Productivity

Track:

Feature delivery time

Lines of frontend code

Duplicate validation

Bug-fix effort

Reliability

Monitor:

Frontend errors

Backend errors

Failed requests

Deployment issues

The goal is not:

"We replaced React."

The goal is:

"We reduced unnecessary complexity while improving the product."

Making the Call

Before replacing an SPA, engineering leaders should ask:

How much client-side state does our product actually require?

How many API endpoints exist primarily to support our own frontend?

How much duplicated validation exists between browser and server?

How large is the JavaScript payload?

Are most workflows forms, lists, dashboards, and CRUD operations?

Would server-rendered HTML solve most of our UI requirements?

Which interactions genuinely require rich client-side behavior?

Most importantly:

Are we using a SPA because the product needs one—or because it became the default architecture?

Final Takeaway

Replacing an SPA with Rails 7 and Hotwire is not about going backward.

It is about reconsidering where application complexity belongs.

The SPA model often looks like:

Browser
   ↓
JavaScript Application
   ↓
API
   ↓
Backend
   ↓
Database

The Hotwire model becomes:

Browser
   ↓
Turbo + Stimulus
   ↓
Rails
   ↓
Database

Turbo provides fast navigation.

Turbo Frames provide focused page updates.

Turbo Streams provide server-driven UI changes.

Stimulus adds lightweight browser behavior.

Rails remains responsible for:

Business logic

Data

Authentication

Authorization

Validation

Routing

Rendering

The result can be a dramatically smaller architectural surface.

The goal is not to eliminate JavaScript. It is to stop sending JavaScript to the browser when the browser does not need it.

For applications built primarily around business workflows, forms, dashboards, commerce, administration, and server-owned data, Rails 7 and Hotwire can offer a remarkably modern development model without requiring a full client-side application.

And perhaps the most important lesson is this:

A web application does not need to behave like a desktop application to feel fast and interactive.

With Turbo, Streams, Frames, and carefully chosen Stimulus controllers, the server can remain the source of truth while the browser still feels immediate.

The future of web development is unlikely to be purely server-side or purely client-side. The strongest architectures will use each side for what it does best—keeping business complexity on the server, keeping interaction close to the user, and refusing to introduce an additional layer when the existing platform already solves the problem well.

Frequently Asked Questions

Hotwire is a set of technologies (including Turbo and Stimulus) that allows Rails applications to provide highly interactive experiences by sending HTML over the wire instead of JSON, reducing the need for large JavaScript frameworks.
Yes, but you use it selectively. Hotwire's Stimulus framework is designed for focused client-side behavior like dropdowns or modals, while letting the server handle core logic and state.
Yes, a migration can be done incrementally. You can start by moving routing back to Rails, replacing API-driven forms with server-rendered ones, and introducing Turbo Frames and Streams for specific page updates.

Need a product built?

We build custom software, mobile apps, and web platforms for startups and enterprises.

Alejandro D.
Vatsalya R.Backend Developer
Gustavo A.
Ganeshan S.Sr. Software Engineer
Fiorella G.
Uptal JoshiSr. Data Scientist

Their team became an extension of ours — within months they'd rebuilt our entire product experience from the ground up.

BitForge
Sr. ArchitectBitForge
Read Case Study