Introduction
The shift from traditional server-side MVC to modern frontend frameworks (Vue.js, React, Angular) represents one of the most significant architectural changes in web development. Understanding what changes โ and what trade-offs you’re making โ is essential for choosing the right approach for your project.
Traditional MVC (Rails, Django, Laravel)
In server-side MVC, the server handles everything:
Browser โ HTTP Request โ del (DB) โ View (Template) โ HTML โ Browser
Model: Business logic and data access (ActiveRecord, Django ORM) View: Templates that render HTML (ERB, Jinja2, Blade) Controller: Handles requests, coordinates model and view
# Rails MVC example
class ArticlesController < ApplicationController
def index
@articles = Article.published.order(created_at: :desc).limit(20)
# @articles is passed to the view template
end
end
<!-- app/views/articles/index.html.erb -->
<% @articles.each do |article| %>
<h2><%= article.title %></h2>
<p><%= article.excerpt %></p>
<% end %>
The server renders complete HTML pages. The browser receives finished HTML and displays it.
Advantages of Server-Side MVC
- Simple mental model โ one data layer, one rendering location
- SEO-friendly โ search engines get complete HTML
- Fast initial page load โ no JavaScript needed to render content
- Simpler state management โ state lives on the server
- Turbolinks โ can feel like an SPA without the complexity
MVVM with Frontend Frameworks (Vue.js, React)
MVVM (Model-View-ViewModel) separates the UI logic into a ViewModel that binds data to the view:
Browser โโ Vue/React Component (ViewModel) โโ API (JSON) โโ Server (Model)
The browser now handles rendering. The server becomes a JSON API.
// Vue.js component โ data, template, and logic in one file
<template>
<div>
<h2 v-for="article in articles" :key="article.id">
{{ article.title }}
r |
| State management | Simple | Complex |
| Team structure | Full-stack | Frontend + Backend |
## Resources
- [Vue.js Documentation](https://vuejs.org/)
- [React Documentation](https://react.dev/)
- [Rails Hotwire](https://hotwired.dev/) โ SPA-like experience without a JS framework
- [Inertia.js](https://inertiajs.com/) โ bridge between server-side and client-side
- [Flux Architecture](https://facebook.github.io/flux/) โ Facebook's data flow pattern
onents
class ArticlesController < ApplicationController
def index
render inertia: 'Articles/Index', props: {
articles: Article.published.as_json(only: [:id, :title, :excerpt])
}
end
end
Summary
| Aspect | Server-Side MVC | Frontend Framework (SPA) |
|---|---|---|
| Rendering | Server | Browser |
| Data layers | 1 (server) | 2 (server + client) |
| SEO | Excellent | Requires SSR/SSG |
| Initial load | Fast | Slower |
| Interactivity | Limited | Excellent |
| Complexity | Lower | Highefline capability (PWA) |
- Mobile app-like experience is required
- You have separate frontend and backend teams
Hybrid Approach (Best of Both)
Modern frameworks blur the line:
- Rails + Hotwire (Turbo + Stimulus): Server-rendered with SPA-like interactivity, no JavaScript framework needed
- Next.js / Nuxt.js: SSR + client-side hydration โ SEO-friendly SPAs
- Inertia.js: Use Vue/React components with server-side routing (no API needed)
# Rails + Inertia.js โ server routing, Vue compwer (download + execute JS bundle)
Navigation: Fast (only fetch data, update DOM)
When to Use Each
Use Server-Side MVC When
- Content is primarily static or read-heavy
- SEO is critical (blogs, marketing sites, e-commerce)
- Team is small and wants simplicity
- You want fast initial page loads
- The application is primarily CRUD operations
Use Frontend Framework (SPA) When
- Highly interactive UI (dashboards, editors, real-time apps)
- Complex state that changes frequently
- You need ofe
- Prerendering: Render pages with a headless browser at build time
6. Performance Trade-offs
Initial load: SPAs are slower โ must download JavaScript bundle before rendering content.
Subsequent navigation: SPAs are faster โ no full page reloads, only data fetches.
Time to Interactive (TTI): SPAs can be slower if the JavaScript bundle is large.
Traditional MVC:
First visit: Fast (server renders HTML)
Navigation: Slower (full page reload each time)
SPA:
First visit: Slo Server-side rendering for SEO
### 5. SEO Challenges
Single-page applications render content with JavaScript. Search engine crawlers may not execute JavaScript, resulting in empty pages:
```html
<!-- What a crawler sees for a Vue SPA -->
<div id="app">
<!-- Empty โ content loaded by JavaScript after page load -->
</div>
Solutions:
- Server-Side Rendering (SSR): Nuxt.js (Vue), Next.js (React) โ render on server, hydrate on client
- Static Site Generation (SSG): Pre-render pages at build timTraditional MVC:** One routing system on the server.
SPA with frontend framework: Two routing systems โ server routes for the API, client-side router for navigation:
// Vue Router โ client-side routing
const routes = [
{ path: '/', component: Home },
{ path: '/articles', component: ArticleList },
{ path: '/articles/:id', component: ArticleDetail },
]
Navigation happens without page reloads, but you need to handle:
- Deep linking (sharing URLs)
- Browser back/forward -articles = articles }, SET_LOADING(state, loading) { state.loading = loading }, SET_ERROR(state, error) { state.error = error } }, actions: { async fetchArticles({ commit }) { commit(‘SET_LOADING’, true) try { const response = await fetch(’/api/articles’) commit(‘SET_ARTICLES’, await response.json()) } catch (err) { commit(‘SET_ERROR’, err.message) } finally { commit(‘SET_LOADING’, false) } } } })
### 4. Routing
**introduces complexity:
- **State management:** Where does shared state live? (Vuex, Redux, Pinia, Zustand)
- **Data fetching:** When to fetch, how to handle loading/error states
- **Optimistic updates:** Update UI before server confirms
- **Cache invalidation:** When is local state stale?
```javascript
// Vuex store โ centralized frontend state management
const store = createStore({
state: {
articles: [],
loading: false,
error: null
},
mutations: {
SET_ARTICLES(state, articles) { state. ['edit', 'delete']
}
</script>
<style scoped>
.address-card { border: 1px solid #ddd; padding: 1rem; }
</style>
Use it anywhere:
<AddressCard
v-for="addr in addresses"
:key="addr.id"
:address="addr"
@edit="handleEdit"
@delete="handleDelete"
/>
3. Two Data Layers
The biggest architectural change: you now have two data layers.
Backend: PostgreSQL โ Rails/Django โ JSON API
Frontend: Vue/React state โ Component tree โ DOM
Data must be synchronized between them. This // fetch results, update DOM manually… });
### 2. Component Reusability
Frontend frameworks enable true component reuse โ HTML, CSS, and JavaScript in a single, portable unit:
```vue
<!-- AddressCard.vue โ reusable component -->
<template>
<div class="address-card">
<p>{{ address.fullAddress }}</p>
<button @click="$emit('edit', address)">Edit</button>
<button @click="$emit('delete', address.id)">Delete</button>
</div>
</template>
<script>
export default {
props: ['address'],
emits:script
// Vue.js v-model: two-way binding
<input v-model="searchQuery" placeholder="Search...">
<p>Results for: {{ searchQuery }}</p>
// When user types, searchQuery updates automatically
// When searchQuery changes programmatically, input updates automatically
This eliminates the jQuery pattern of manually selecting elements and updating them:
// Old jQuery approach
$('#search').on('input', function() {
const query = $(this).val();
$('#results-label').text('Results for: ' + query);
</div>
</template>
<script>
export default {
data() {
return { articles: [] }
},
async mounted() {
const response = await fetch('/api/articles')
this.articles = await response.json()
}
}
</script>
Key Differences
1. Data Binding
Traditional MVC: Data flows one way โ server renders data into HTML, browser displays it. User interactions require a new request.
MVVM: Two-way data binding โ changes in the UI automatically update the data model, and vice versa:
Comments