Multi-Page Application (MPA)
A Multi-Page Application is the traditional web architecture where each route corresponds to a separate HTML page loaded from the server. Every navigation triggers a full page reload. This is the "thin client" approach—the server does most of the work, and the browser primarily renders HTML.
Core Concept
In an MPA, the server generates a complete HTML page for each request. Navigation means loading a new page from the server.
Multi-Page App (MPA)
- User clicks link
- Browser requests URL from server
- Server renders complete HTML
- Browser downloads page
- Page displays
- Full page reload
Time: 1-3 seconds
JavaScript: Optional
Single Page App (SPA)
- User clicks link
- JavaScript intercepts
- Fetch data from API
- Update DOM with content
- No page reload
Time: 100-500ms
JavaScript: Required
Basic MPA Architecture
// MULTI-PAGE APPLICATION (MPA)
// Traditional web development: Each route is a separate HTML file
// Server-side (Node.js/Express example)
const express = require('express');
const app = express();
// Each route renders a complete HTML page
app.get('/', (req, res) => {
res.render('home', {
title: 'Home',
user: req.session.user
});
});
app.get('/products', async (req, res) => {
const products = await db.products.findAll();
res.render('products', {
title: 'Products',
products: products
});
});
app.get('/products/:id', async (req, res) => {
const product = await db.products.findById(req.params.id);
res.render('product-detail', {
title: product.name,
product: product
});
});
app.get('/cart', (req, res) => {
res.render('cart', {
title: 'Shopping Cart',
cart: req.session.cart
});
});
// Each navigation = full page reload
// Browser requests HTML from server
// Server renders complete page
// Browser displays new page
/* KEY CHARACTERISTICS:
- Each URL = separate HTML page
- Server renders complete HTML
- Full page reload on navigation
- Browser back/forward work natively
- SEO-friendly by default
*/ Server-Side Templating
MPAs use template engines to generate HTML on the server with data from the database.
// MPA TEMPLATING EXAMPLE
// Server-side templates generate HTML
// views/layout.ejs - Shared layout
<!DOCTYPE html>
<html>
<head>
<title><%= title %> - My Shop</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/products">Products</a>
<a href="/cart">Cart</a>
<% if (user) { %>
<span>Hello, <%= user.name %></span>
<a href="/logout">Logout</a>
<% } else { %>
<a href="/login">Login</a>
<% } %>
</nav>
<main>
<%- body %>
</main>
<footer>
<p>© 2024 My Shop</p>
</footer>
<!-- Minimal JavaScript for enhancement only -->
<script src="/app.js"></script>
</body>
</html>
// views/products.ejs - Products page
<h1>Products</h1>
<form method="GET" action="/products">
<input type="search"
name="q"
value="<%= query %>"
placeholder="Search products...">
<button type="submit">Search</button>
</form>
<div class="products">
<% products.forEach(product => { %>
<div class="product-card">
<img src="<%= product.image %>" alt="<%= product.name %>">
<h2><%= product.name %></h2>
<p><%= product.description %></p>
<p class="price">$<%= product.price %></p>
<form method="POST" action="/cart/add">
<input type="hidden" name="productId" value="<%= product.id %>">
<button type="submit">Add to Cart</button>
</form>
</div>
<% }); %>
</div>
/* FLOW:
1. User clicks "Products" link
2. Browser sends GET /products
3. Server queries database
4. Server renders products.ejs with data
5. Server sends complete HTML to browser
6. Browser parses HTML, loads CSS/images
7. Page displayed
8. Minimal JavaScript adds enhancements
Total time: 1-3 seconds
But: SEO perfect, works without JavaScript
*/ Form Handling
Forms in MPAs submit to the server, which processes the data and returns a new page.
// FORM HANDLING IN MPAs
// Forms submit to server, page reloads with result
// Add to cart form
<form method="POST" action="/cart/add">
<input type="hidden" name="productId" value="123">
<input type="number" name="quantity" value="1" min="1">
<button type="submit">Add to Cart</button>
</form>
// Server handles form submission
app.post('/cart/add', (req, res) => {
const { productId, quantity } = req.body;
// Validate on server (never trust client!)
if (quantity < 1 || quantity > 99) {
return res.render('error', {
message: 'Invalid quantity'
});
}
// Add to cart
req.session.cart = req.session.cart || [];
req.session.cart.push({
productId: productId,
quantity: parseInt(quantity)
});
// Redirect to cart page
res.redirect('/cart');
// Browser loads new page
});
// PROGRESSIVE ENHANCEMENT with JavaScript
document.addEventListener('DOMContentLoaded', () => {
const forms = document.querySelectorAll('form[data-ajax]');
forms.forEach(form => {
form.addEventListener('submit', async (e) => {
e.preventDefault();
try {
// Submit via AJAX (progressive enhancement)
const formData = new FormData(form);
const response = await fetch(form.action, {
method: form.method,
body: formData
});
if (response.ok) {
// Update cart count without page reload
updateCartCount();
showNotification('Added to cart!');
}
} catch (error) {
// Fallback: submit normally
form.submit();
}
});
});
});
/* BENEFITS:
- Works without JavaScript
- Server validates everything
- Fallback to standard form submission
- Progressive enhancement adds AJAX
*/ SEO Advantage
MPAs are inherently SEO-friendly because every page is complete HTML that search engines can easily crawl.
// SEO ADVANTAGE OF MPAs
// Each page is a complete HTML document
// Search engines see everything immediately
// /products/123 returns:
<!DOCTYPE html>
<html>
<head>
<title>Wireless Headphones - My Shop</title>
<meta name="description" content="Premium noise-cancelling wireless headphones...">
<meta property="og:title" content="Wireless Headphones">
<meta property="og:image" content="/images/headphones.jpg">
<link rel="canonical" href="https://myshop.com/products/123">
<!-- Structured data for rich snippets -->
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "Product",
"name": "Wireless Headphones",
"image": "/images/headphones.jpg",
"description": "Premium noise-cancelling...",
"brand": "AudioTech",
"offers": {
"@type": "Offer",
"price": "299.99",
"priceCurrency": "USD"
}
}
</script>
</head>
<body>
<article>
<h1>Wireless Headphones</h1>
<img src="/images/headphones.jpg" alt="Wireless Headphones">
<p>Premium noise-cancelling wireless headphones...</p>
<p class="price">$299.99</p>
<!-- Real content, not JavaScript placeholders -->
<div class="reviews">
<h2>Customer Reviews</h2>
<div class="review">
<strong>John D.</strong> - ⭐⭐⭐⭐⭐
<p>Best headphones I've ever owned!</p>
</div>
<!-- More reviews... -->
</div>
<div class="related">
<h2>Related Products</h2>
<a href="/products/124">Bluetooth Speaker</a>
<a href="/products/125">Phone Charger</a>
</div>
</article>
</body>
</html>
/* WHAT SEARCH ENGINES SEE:
✓ Complete page content
✓ Meta tags for social sharing
✓ Structured data for rich results
✓ Internal links to crawl
✓ Images with alt text
✓ Semantic HTML
No special SEO setup needed!
No server-side rendering complexity!
Works the same for users and bots!
*/
// Compare to SPA (BAD for SEO):
<!DOCTYPE html>
<html>
<head>
<title>My Shop</title>
</head>
<body>
<div id="root"></div> <!-- EMPTY! -->
<script src="/bundle.js"></script>
</body>
</html>
/* WHAT SEARCH ENGINES SEE:
✗ No content
✗ No meta tags
✗ No links
✗ No images
✗ Must execute JavaScript (maybe)
Requires SSR or pre-rendering!
*/ SEO Benefits
- No special setup - Works out of the box
- Complete HTML - Search engines see everything
- Meta tags - Each page has its own title, description
- Structured data - Rich snippets naturally supported
- Social sharing - Open Graph tags work perfectly
- Fast indexing - No JavaScript execution needed
State Persistence
Since pages reload, state must be stored on the server or in browser storage.
// STATE PERSISTENCE IN MPAs
// State doesn't persist in memory (page reloads)
// Must use server session, cookies, or localStorage
// Problem: Shopping cart disappears on page reload
// Solution: Store in server session
// Server-side session
const session = require('express-session');
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: true,
cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 hours
}));
// Add to cart
app.post('/cart/add', (req, res) => {
req.session.cart = req.session.cart || [];
req.session.cart.push(req.body.product);
// Cart persists across page reloads
res.redirect('/cart');
});
// View cart
app.get('/cart', (req, res) => {
const cart = req.session.cart || [];
res.render('cart', { cart });
});
// Client-side enhancement with localStorage
// For instant feedback before page reload
document.addEventListener('DOMContentLoaded', () => {
// Load cart count from localStorage
const cartCount = localStorage.getItem('cartCount') || 0;
document.getElementById('cart-count').textContent = cartCount;
});
function addToCart(productId) {
// Update localStorage immediately (optimistic)
let cartCount = parseInt(localStorage.getItem('cartCount')) || 0;
cartCount++;
localStorage.setItem('cartCount', cartCount);
document.getElementById('cart-count').textContent = cartCount;
// Submit form to server (authoritative)
document.getElementById('add-to-cart-form').submit();
}
// Hybrid approach: User preferences
app.get('/settings', (req, res) => {
// Load from database
const settings = await db.users.findById(req.session.userId);
res.render('settings', { settings });
});
app.post('/settings', (req, res) => {
// Save to database
await db.users.update(req.session.userId, req.body);
res.redirect('/settings');
});
// Client-side: Cache in localStorage for instant UI updates
const settings = JSON.parse(localStorage.getItem('settings') || '{}');
document.body.className = settings.theme || 'light';
// But server is still source of truth Performance Optimization
MPAs can be optimized to feel nearly as fast as SPAs.
// PERFORMANCE CONSIDERATIONS FOR MPAs
// Challenge: Full page reload for every navigation
// - Download HTML (10-50KB)
// - Download CSS (may be cached)
// - Download JavaScript (may be cached)
// - Download images
// - Parse and render
// OPTIMIZATION 1: Cache static assets
<head>
<link rel="stylesheet" href="/styles.css">
<!-- Browser caches this for future page loads -->
<script src="/app.js"></script>
<!-- Also cached -->
</head>
// Server sets cache headers
app.use('/styles.css', (req, res, next) => {
res.setHeader('Cache-Control', 'public, max-age=31536000');
next();
});
// OPTIMIZATION 2: HTTP/2 multiplexing
// Multiple resources download in parallel
// Faster than sequential HTTP/1.1
// OPTIMIZATION 3: Prefetching next page
<link rel="prefetch" href="/products">
<!-- Browser prefetches in background -->
document.addEventListener('DOMContentLoaded', () => {
// Prefetch on hover
const links = document.querySelectorAll('a');
links.forEach(link => {
link.addEventListener('mouseenter', () => {
const prefetch = document.createElement('link');
prefetch.rel = 'prefetch';
prefetch.href = link.href;
document.head.appendChild(prefetch);
});
});
});
// OPTIMIZATION 4: Progressive enhancement
// Show cached content while fetching fresh data
// Service Worker caching
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(cached => {
// Return cached version immediately
if (cached) {
return cached;
}
// Fetch from network
return fetch(event.request);
})
);
});
// OPTIMIZATION 5: Partial page updates (PJAX)
// Progressive enhancement: update only <main> content
document.addEventListener('click', (e) => {
if (e.target.tagName === 'A') {
e.preventDefault();
fetch(e.target.href)
.then(r => r.text())
.then(html => {
// Parse HTML
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Update only main content
document.querySelector('main').innerHTML =
doc.querySelector('main').innerHTML;
// Update URL
history.pushState({}, '', e.target.href);
// Update title
document.title = doc.title;
})
.catch(() => {
// Fallback: normal navigation
window.location.href = e.target.href;
});
}
});
// Now MPA has SPA-like navigation!
// But still works without JavaScript Modern Hybrid Approaches
Contemporary frameworks combine MPA and SPA benefits.
// HYBRID: MPA + SPA Benefits
// Modern meta-frameworks combine both approaches
// APPROACH 1: MPA with Islands (Astro)
// Most of the page is static HTML
// "Islands" of interactivity use JavaScript
// products.astro
---
const products = await db.products.findAll();
---
<Layout title="Products">
<h1>Products</h1>
<!-- Static HTML -->
{products.map(product => (
<div class="product">
<h2>{product.name}</h2>
<p>{product.description}</p>
<!-- Interactive island -->
<AddToCartButton
client:load
productId={product.id}
/>
</div>
))}
</Layout>
// AddToCartButton.jsx - Only this part uses JavaScript
import { useState } from 'react';
export default function AddToCartButton({ productId }) {
const [added, setAdded] = useState(false);
const handleClick = async () => {
await fetch('/api/cart/add', {
method: 'POST',
body: JSON.stringify({ productId })
});
setAdded(true);
};
return (
<button onClick={handleClick}>
{added ? 'Added!' : 'Add to Cart'}
</button>
);
}
// Result:
// - Page loads as static HTML (fast, SEO-friendly)
// - Only interactive parts load JavaScript
// - Best of both worlds!
// APPROACH 2: MPA that morphs to SPA (Turbo/Hotwire)
// First navigation: Full page load
// Subsequent: Fetch HTML via AJAX, swap content
<a href="/products" data-turbo="true">Products</a>
// Turbo intercepts clicks
// Fetches /products HTML
// Swaps <main> content
// Updates URL
// No JavaScript frameworks needed!
// APPROACH 3: Progressively enhanced MPA
// Works as MPA by default
// Enhances to SPA when JavaScript available
// base.html
<html>
<body>
<nav>
<a href="/">Home</a>
<a href="/products">Products</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<script>
// Progressive enhancement
if ('fetch' in window) {
enhanceNavigation();
}
</script>
</body>
</html>
function enhanceNavigation() {
document.querySelectorAll('nav a').forEach(link => {
link.addEventListener('click', async (e) => {
e.preventDefault();
const response = await fetch(link.href);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Update content
document.querySelector('main').innerHTML =
doc.querySelector('main').innerHTML;
// Update URL
history.pushState({}, '', link.href);
});
});
}
// Now: MPA that feels like SPA
// But degrades gracefully Benefits
- SEO-friendly - Works perfectly with search engines
- Works without JavaScript - Progressive enhancement friendly
- Simple architecture - Traditional, well-understood patterns
- Browser features work - Back button, bookmarks, refresh
- Lower device requirements - Less client-side processing
- Better accessibility - Screen readers handle better
- Easier debugging - View source shows actual content
- Smaller JavaScript bundles - Or none at all
Drawbacks
- Full page reloads - Slower perceived navigation
- State doesn't persist - Must use sessions/cookies
- Higher server load - Server renders every page
- Less interactive - Limited to page reloads for updates
- Flash of content - Visible page transitions
- Bandwidth usage - Re-downloads HTML each time
When to Use MPAs
- Content websites - Blogs, news, documentation
- E-commerce - Product pages, catalogs (SEO critical)
- Marketing sites - Landing pages, company websites
- Public content - Anything that needs to be discoverable
- Simple applications - CRUD apps, admin panels
- Government/civic sites - Accessibility requirements
- When JavaScript can't be guaranteed - Maximum compatibility
When NOT to Use MPAs
- Rich interactive apps - Gmail, Figma, games
- Real-time collaboration - Needs persistent connection
- Complex state management - State across many interactions
- Offline capabilities - Needs Service Workers + client logic
- App-like UX needed - Instant feedback, no reloads
Popular MPA Frameworks/Technologies
| Technology | Language | Best For |
|---|---|---|
| Ruby on Rails | Ruby | Rapid development |
| Django | Python | Content management |
| Laravel | PHP | E-commerce, portals |
| Express + EJS | Node.js | Simple apps |
| ASP.NET MVC | C# | Enterprise apps |
MPA vs SPA Comparison
| Aspect | MPA | SPA |
|---|---|---|
| SEO | ✅ Perfect | ⚠️ Needs SSR/SSG |
| Initial Load | ✅ Fast | ❌ Slow |
| Navigation | ❌ Potentially Slow (reload) | ✅ Likely Fast (device dependent) |
| JavaScript | ✅ Optional | ❌ Required |
| Complexity | ✅ Simple | ❌ Complex |
| Offline | ✅ Possible | ✅ Possible |
The Modern Trend: Hybrid
Most modern frameworks combine MPA and SPA:
- Next.js - MPA with SSR, becomes SPA after hydration
- Nuxt - Vue equivalent of Next.js
- SvelteKit - Svelte with MPA/SPA hybrid
- Astro - MPA with optional islands of interactivity
- Remix - Focused on progressive enhancement
Related Patterns
- Single Page Application (SPA) - The opposite approach
- Client-Server - Architectural foundation
- Progressive Enhancement - MPA enhancement strategy
- MVC - Common pattern for organizing MPAs