Adaptable MVC

Progressive enhancement — serving HTML and JSON from a single codebase

The Adaptable Approach

What if your application could work both ways? Serve complete HTML pages for browsers (and users with JavaScript disabled), but return JSON for JavaScript-powered clients? This is the adaptable MVC approach — the same controller handles both scenarios.

Adaptable MVC: Two Paths, One Backend

Path A: JavaScript Enabled (Enhanced Experience)
+----------+  fetch('/api/users')  +------------+            +-------+
| Browser  |  Accept: app/json     |            |   Query    |       |
| (JS on)  | --------------------> | Controller | ---------> | Model | <--> DB
|          | <-------------------- |            | <--------- |       |
| Renders  |  JSON response        | Checks     |   Data     +-------+
| via DOM  |                       | Accept hdr |
+----------+                       +------------+

Path B: No JavaScript (Baseline Experience)
+---------+  GET /users            +------------+            +-------+
| Browser |  Accept: text/html     |            |   Query    |       |
| (JS off)| -------------------->  | Controller | ---------> | Model | <--> DB
|         | <--------------------  |            | <--------- |       |
| Displays|  Complete HTML page    | Checks     |   Data     +-------+
| HTML    |                        | Accept hdr |
+---------+                        +------------+

The key insight is that both paths share the same controller and model. The only difference is what the controller returns: HTML or JSON. The decision is made by inspecting the request.

Content Negotiation with the Accept Header

The controller inspects the request's Accept header (or the X-Requested-With header) to determine what format to respond with. This is the same content negotiation mechanism used in REST APIs.

When a browser navigates to a URL directly, it sends Accept: text/html. When JavaScript calls fetch() with JSON headers, it sends Accept: application/json. The controller uses this to decide its response format.

Node.js (Express) Example

// Express controller that adapts its response format app.get('/users', async (req, res) => { const users = await User.getAll(); if (req.accepts('html')) { // Browser request -> render full HTML page res.render('users/index', { users }); } else { // API / fetch() request -> return JSON res.json(users); } }); app.post('/users', async (req, res) => { const user = await User.create(req.body); if (req.accepts('html')) { // Form submission -> redirect to user list res.redirect('/users'); } else { // API call -> return created user as JSON res.status(201).json(user); } });

PHP Example

getAll(); $accept = $_SERVER['HTTP_ACCEPT'] ?? 'text/html'; $isAjax = ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest'; if ($isAjax || str_contains($accept, 'application/json')) { // JavaScript client -> return JSON header('Content-Type: application/json'); echo json_encode($users); } else { // Browser -> render full HTML page require 'views/users/index.php'; } ?>

Progressive Enhancement in Practice

In the adaptable approach, the JavaScript enhancement layer is optional. The HTML pages work perfectly on their own. When JavaScript is available, it intercepts form submissions and link clicks, replaces them with fetch() calls, and updates the DOM without a full page reload.

This means:

  • Without JavaScript: forms submit normally, pages reload, everything works through standard HTML
  • With JavaScript: forms are intercepted, data is sent via fetch(), and the page updates dynamically

The server does not need to know or care which path the client takes. It simply responds with the appropriate format based on the Accept header.

Three-Way Comparison

Aspect Server-Side MVC Client-Side MVC (SPA) Adaptable MVC
Rendering Server only Browser only Both (server baseline, browser enhanced)
Response format HTML JSON HTML or JSON (based on Accept header)
JavaScript required No Yes No (works without, enhanced with)
SEO Excellent Poor Excellent
Interactivity Low (page reloads) High (instant updates) Progressive (basic without JS, rich with JS)
Complexity Low Medium Medium (two response paths in controller)
Accessibility Best (standard HTML) Requires extra effort Best (falls back to standard HTML)

When to Use Each Approach

  • Server-Side MVC — when simplicity and SEO matter most. Content sites, blogs, admin panels, form-heavy workflows. Works everywhere, no JavaScript bundle required.
  • Client-Side MVC (SPA) — when rich interactivity is essential and SEO is not a primary concern. Real-time dashboards, collaborative tools, chat apps, desktop-like experiences.
  • Adaptable MVC — when you want the best of both worlds. The baseline experience works without JavaScript (accessible, SEO-friendly), but JavaScript enhances it with smooth transitions and partial updates. This is the progressive enhancement philosophy applied to application architecture.