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.

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.
The classic SPA architecture looks something like this:
Browser
↓
React / Vue / Angular
↓
Client State
↓
API
↓
Backend
↓
DatabaseIt 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
↓
DatabaseThen 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.
Rails has always been particularly good at building server-driven applications.
Rails 7 modernizes that approach through Hotwire.
Instead of:
Browser
↓
JavaScript Application
↓
API
↓
Serverthe architecture can become:
Browser
↓
Turbo + Stimulus
↓
Rails
↓
DatabaseHotwire 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.
Consider a simple form.
User
↓
React Form
↓
API Request
↓
JSON Response
↓
Client State
↓
React RenderUser
↓
HTML Form
↓
Rails
↓
Validation / Database
↓
HTML Response
↓
Turbo UpdateThe 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 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 UpdateFrom 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 allow a page to be divided into independently replaceable sections.
Imagine an account page:
Account
│
├── Profile
├── Subscription
├── Billing
└── NotificationsIf 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 FrameThis 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 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 CommentThe 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.
Consider an operations dashboard.
A new order arrives:
New Order
↓
Rails
↓
Broadcast
↓
Turbo Stream
↓
Update Order ListThe 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.
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 InteractionInstead of:
Entire Application
↓
Large JavaScript Runtime
↓
Everything Runs Client-SideThis 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 are where this architecture can become particularly compelling.
In a traditional SPA:
Form
↓
JavaScript
↓
API
↓
JSON
↓
Client State
↓
UI UpdateWith Rails and Hotwire:
Form
↓
Rails Controller
↓
Validation
↓
Database
↓
HTML / Turbo Response
↓
UI UpdateRails 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.
A server-driven architecture can also simplify security boundaries.
Instead of:
Browser
↓
Token
↓
API
↓
Authorizationa Rails application can use its established server-side session and authorization mechanisms:
Browser
↓
Rails Session
↓
Authorization
↓
Controller / Policy
↓
HTMLThis 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.
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
↓
RenderA server-driven application can move much of that work to the server:
Request
↓
Rails
↓
HTML
↓
BrowserThen 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.
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 InteractionThat 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.
If every feature still requires extensive JavaScript state and API calls, the migration may simply move complexity around.
Not every form, button, or page needs a JSON endpoint.
Use server-rendered HTML where it makes sense.
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.
Moving work to Rails means the server becomes more important.
Optimize:
Database queries
Caching
Rendering
Background jobs
Server response time
A complete rewrite introduces unnecessary risk.
Incremental migration is usually easier to validate.
Some interactions genuinely belong in the browser.
The goal is not zero JavaScript.
The goal is purposeful JavaScript.
A production application can remain surprisingly compact:
Browser
│
┌───────────┴───────────┐
▼ ▼
Turbo Stimulus
│ │
└───────────┬───────────┘
▼
Rails
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Controllers Services Models
│ │ │
└──────────────┼──────────────┘
▼
DatabaseFor live updates:
Rails
↓
Turbo Streams
↓
Broadcast
↓
BrowserThis keeps the architecture cohesive.
Instead of maintaining:
Frontend Repository
+
Backend Repository
+
API Contract
+
Client State
+
Deployment Coordinationa team can often keep much of the application in one place.
A migration does not need to start with a rewrite.
Start with pages dominated by:
Forms
Tables
CRUD
Content
Business workflows
These are often excellent candidates.
Allow Rails to own routes that do not require complex client-side navigation.
Use Rails forms and Turbo responses.
Identify page sections that need independent updates.
Use them for targeted updates and real-time workflows.
Move only necessary interactions into Stimulus controllers.
If the server already owns the authoritative state, do not duplicate it in the browser without a reason.
Compare the old and new architectures rather than relying on assumptions.
A successful migration should produce measurable improvements.
Track:
JavaScript bundle size
Number of client components
Client-side state stores
API endpoints
Measure:
Largest Contentful Paint
Interaction to Next Paint
Time to First Byte
Page navigation time
Track:
Feature delivery time
Lines of frontend code
Duplicate validation
Bug-fix effort
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."
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?
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
↓
DatabaseThe Hotwire model becomes:
Browser
↓
Turbo + Stimulus
↓
Rails
↓
DatabaseTurbo 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.
We build custom software, mobile apps, and web platforms for startups and enterprises.



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