Client-Side MVC (SPA)

Moving rendering to the browser with JavaScript and JSON APIs

SPA Architecture

In client-side MVC (also called the Single-Page Application or SPA pattern), the server acts purely as a data API. It sends JSON, and the browser's JavaScript takes full responsibility for rendering the user interface.

Client-Side MVC (SPA) Flow:

+---------------------------+                    +--------------------------+
|        Browser            |                    |         Server           |
|                           |                    |                          |
|  +---------------------+  |   fetch() / XHR    |  +--------------------+  |
|  |   JavaScript App    |  | -----------------> |  |    Controller      |  |
|  |                     |  |                    |  |                    |  |
|  |  +---------------+  |  |                    |  |  Receives request  |  |
|  |  |  View (React, |  |  |                    |  |  Calls Model       |  |
|  |  |  Vue, Angular) |  |  |                    |  +--------+-----------+  |
|  |  |               |  |  |                    |           |              |
|  |  |  Renders DOM  |  |  |      JSON          |  +--------v-----------+  |
|  |  |  in browser   |  |  | <----------------- |  |      Model         |  |
|  |  +---------------+  |  |                    |  |                    |  |
|  +---------------------+  |                    |  |  Queries DB        |  |
|                           |                    |  |  Returns data      |  |
+---------------------------+                    |  +--------------------+  |
                                                 |                          |
                                                 |         Database <-->    |
                                                 +--------------------------+

Step-by-Step Flow

  1. Browser loads a minimal HTML shell — the initial page is mostly empty, with a <script> tag that loads the JavaScript application bundle
  2. JavaScript app initializes — the framework (React, Vue, Angular) boots up and takes control of the page
  3. App makes API request — the JavaScript calls fetch('/api/users') to request data from the server
  4. Server Controller processes request — the server receives the request, calls the Model, and returns JSON data
  5. Model queries database — the model fetches data and returns it; the controller sends it as a JSON response
  6. JavaScript renders the DOM — the client-side View receives the JSON and builds/updates the HTML in the browser using DOM manipulation

Key characteristic: The server returns JSON data only. The client-side JavaScript is responsible for all rendering. Navigation between "pages" happens entirely in the browser without full page reloads.

Client-Side MVC Components

In a SPA, the MVC roles shift to the browser. Here is how each component maps to client-side JavaScript:

Model: Data Fetching with fetch()

The client-side Model manages data by making HTTP requests to the server's JSON API. It has no knowledge of the DOM or how data is displayed:

// Client-side Model - handles all data access const StoryModel = { async getAll() { const res = await fetch('/api/stories'); return res.json(); }, async create(data) { const res = await fetch('/api/stories', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return res.json(); }, async delete(id) { await fetch(`/api/stories/${id}`, { method: 'DELETE' }); } };

View: DOM Manipulation

The client-side View builds and updates HTML elements in the browser. It receives data and produces DOM nodes, but does not fetch data or handle events:

// Client-side View - renders data into the DOM const StoryView = { renderList(stories, container) { container.innerHTML = ''; // clear previous content stories.forEach(story => { const row = document.createElement('tr'); const titleCell = document.createElement('td'); titleCell.textContent = story.title; // XSS safe row.appendChild(titleCell); const statusCell = document.createElement('td'); statusCell.textContent = story.status; row.appendChild(statusCell); container.appendChild(row); }); } };

Controller: Event Handling

The client-side Controller listens for user events (clicks, form submissions), calls the Model to get or save data, and passes the result to the View:

// Client-side Controller - coordinates Model and View async function init() { const tbody = document.querySelector('#story-table tbody'); // Load and display stories const stories = await StoryModel.getAll(); StoryView.renderList(stories, tbody); // Handle form submission document.querySelector('#story-form').addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData); await StoryModel.create(data); // Refresh the list const updated = await StoryModel.getAll(); StoryView.renderList(updated, tbody); e.target.reset(); }); } init();

XSS Safety: textContent vs innerHTML

In server-side MVC, template engines handle HTML escaping automatically. In client-side MVC, you are responsible for preventing XSS. The critical distinction is between textContent and innerHTML:

// SAFE: textContent treats the value as plain text cell.textContent = story.title; // If title is "", it displays as literal text // DANGEROUS: innerHTML parses the value as HTML cell.innerHTML = story.title; // If title is "", it EXECUTES the script

Server-Side vs Client-Side MVC

Aspect Server-Side MVC Client-Side MVC (SPA)
Rendering Server renders HTML Browser renders via JavaScript
Data format Complete HTML pages JSON from API
Navigation Full page reload In-browser, no reload
Initial load Fast (HTML ready immediately) Slower (must load JS bundle first)
SEO Excellent (HTML is crawlable) Poor (requires SSR workaround)
JavaScript required No Yes
Interactivity Limited (requires page reload) Rich (instant updates)
XSS prevention Template engine escaping textContent (manual)

When to Use Client-Side MVC

  • Highly interactive UIs — real-time dashboards, collaborative editors, chat applications
  • Desktop-like experiences — apps where constant page reloads would feel jarring (e.g., email clients, project management tools)
  • Offline-capable applications — SPAs can cache data locally and sync when reconnected
  • Separate frontend/backend teams — the API contract (REST endpoints) is the only shared interface

Client-Side Frameworks

Framework Approach
React Component-based, virtual DOM
Vue Reactive data binding, single-file components
Angular Full framework with dependency injection, RxJS