COMP 305: Object-Oriented Software Design

University of San Diego, Fall 2025

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)

  1. User clicks link
  2. Browser requests URL from server
  3. Server renders complete HTML
  4. Browser downloads page
  5. Page displays
  6. Full page reload

Time: 1-3 seconds
JavaScript: Optional

Single Page App (SPA)

  1. User clicks link
  2. JavaScript intercepts
  3. Fetch data from API
  4. Update DOM with content
  5. 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>&copy; 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

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

Drawbacks

When to Use MPAs

When NOT to Use MPAs

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:

Related Patterns