Skip to main content
โšก Calmops

MVC vs MVVM: Traditional Rails vs Modern Frontend Frameworks

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