Skip to main content

MVC vs MVVM: Traditional Rails vs Modern Frontend Frameworks

Created: December 1, 2017 Larry Qu 5 min read

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:
```yaml 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 },
]
```javascript
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?
// 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:

## 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:

```java    </h2>

Comments

Share this article

Scan to read on mobile

👍 Was this article helpful?