Single-Page Applications (SPA) vs Multi-Page Applications (MPA)
Introduction
In this tutorial, you will learn the fundamental differences between Multi-Page Applications (MPA) and Single-Page Applications (SPA), and how to implement client-side routing using Navigo - a simple, lightweight JavaScript router.
What You'll Learn
- Understand the architecture of Multi-Page Applications
- Understand the architecture of Single-Page Applications
- Learn the pros and cons of each approach
- Build a simple MPA
- Convert it to an SPA
- Implement client-side routing with Navigo
- Handle route parameters and navigation
- Integrate routing with your component-based architecture
Prerequisites
- Basic HTML, CSS, and JavaScript
- Understanding of DOM manipulation
- Familiarity with ES6 modules
- (Optional) Component-based architecture from previous exercises
Part 1: Understanding Multi-Page Applications (MPA)
What is an MPA?
A Multi-Page Application is the traditional web architecture where:
- Each page is a separate HTML file
- Clicking a link loads a completely new page from the server
- The browser makes a full page reload
- Each page has its own URL and HTML document
MPA Architecture
User clicks link → Browser requests new page → Server sends HTML → Browser reloads entire page
Example MPA Structure:
website/
├── index.html # Home page
├── about.html # About page
├── products.html # Products page
├── contact.html # Contact page
├── styles.css # Shared styles
└── script.js # Shared scripts
Part 2: Understanding Single-Page Applications (SPA)
What is an SPA?
A Single-Page Application is a modern web architecture where:
- One HTML file serves the entire application
- Content changes are handled by JavaScript
- No full page reloads when navigating
- URLs are simulated using JavaScript routing
- Feels more like a native app
SPA Architecture
User clicks link → JavaScript intercepts → Updates URL (History API) → Updates DOM → No server request
Example SPA Structure:
website/
├── index.html # Single HTML file
├── styles.css
├── app.js # Main application
├── router.js # Client-side routing
├── HomePage.js # Home view component
├── AboutPage.js # About view component
├── ProductsPage.js # Products view component
└── ContactPage.js # Contact view component
Part 3: Build a Simple Multi-Page Application
Let's build a simple task management MPA with three pages.
Step 1: Create the Home Page
Create index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager - Home</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<nav class="navbar">
<div class="container">
<h1>📝 Task Manager</h1>
<ul class="nav-links">
<li><a href="index.html" class="active">Home</a></li>
<li><a href="tasks.html">Tasks</a></li>
<li><a href="about.html">About</a></li>
</ul>
</div>
</nav>
<main class="container">
<div class="hero">
<h2>Welcome to Task Manager</h2>
<p>Manage your tasks efficiently and stay productive!</p>
<a href="tasks.html" class="btn">View Tasks</a>
</div>
</main>
<footer class="footer">
<p>© 2026 Task Manager. All rights reserved.</p>
</footer>
</body>
</html>
Step 2: Create the Tasks Page
Create tasks.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager - Tasks</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<nav class="navbar">
<div class="container">
<h1>📝 Task Manager</h1>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="tasks.html" class="active">Tasks</a></li>
<li><a href="about.html">About</a></li>
</ul>
</div>
</nav>
<main class="container">
<h2>My Tasks</h2>
<div id="task-list">
<div class="task">
<input type="checkbox">
<span>Complete JavaScript tutorial</span>
</div>
<div class="task">
<input type="checkbox">
<span>Build a SPA application</span>
</div>
<div class="task">
<input type="checkbox">
<span>Learn about routing</span>
</div>
</div>
</main>
<footer class="footer">
<p>© 2026 Task Manager. All rights reserved.</p>
</footer>
</body>
</html>
Step 3: Create the About Page
Create about.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager - About</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<nav class="navbar">
<div class="container">
<h1>Task Manager</h1>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="tasks.html">Tasks</a></li>
<li><a href="about.html" class="active">About</a></li>
</ul>
</div>
</nav>
<main class="container">
<h2>About Task Manager</h2>
<p>Task Manager is a simple application to help you organize your daily tasks.</p>
<p>Built with HTML, CSS, and JavaScript.</p>
</main>
<footer class="footer">
<p>© 2026 Task Manager. All rights reserved.</p>
</footer>
</body>
</html>
Step 4: Create Shared Styles
Create styles.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.navbar {
background: #667eea;
color: white;
padding: 1rem 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.navbar .container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.navbar h1 {
font-size: 1.5rem;
}
.nav-links {
list-style: none;
display: flex;
gap: 2rem;
}
.nav-links a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background 0.3s;
}
.nav-links a:hover,
.nav-links a.active {
background: rgba(255, 255, 255, 0.2);
}
main.container {
max-width: 1200px;
margin: 2rem auto;
padding: 0 2rem;
flex: 1;
}
.hero {
text-align: center;
padding: 4rem 2rem;
}
.hero h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
color: #333;
}
.hero p {
font-size: 1.2rem;
color: #666;
margin-bottom: 2rem;
}
.btn {
display: inline-block;
padding: 1rem 2rem;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 8px;
transition: background 0.3s;
margin: 0.5rem;
}
.btn:hover {
background: #5568d3;
}
#task-list {
margin-top: 2rem;
}
.task {
padding: 1rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 1rem;
}
.task input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.footer {
background: #f8f9fa;
padding: 2rem;
text-align: center;
color: #666;
margin-top: auto;
}
Step 5: Test Your MPA
- Open
index.htmlin your browser - Click the navigation links
- Notice: The page fully reloads each time (watch the browser tab icon)
- Notice: The entire page flashes white during navigation
- Notice: Browser history works automatically
Key Observation: Every click triggers a full page reload from the server.
Part 4: Convert to a Single-Page Application
Now let's convert the MPA to an SPA using vanilla JavaScript (no router yet).
Step 1: Create a Single HTML File
Create spa-index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager - SPA</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<nav class="navbar">
<div class="container">
<h1>📝 Task Manager</h1>
<ul class="nav-links">
<li><a href="#/" data-link>Home</a></li>
<li><a href="#/tasks" data-link>Tasks</a></li>
<li><a href="#/about" data-link>About</a></li>
</ul>
</div>
</nav>
<main id="app" class="container">
<!-- Content will be dynamically loaded here -->
</main>
<footer class="footer">
<p>© 2026 Task Manager. All rights reserved.</p>
</footer>
<script type="module" src="spa-app.js"></script>
</body>
</html>
Step 2: Create the SPA JavaScript
Create spa-app.js:
// spa-app.js
// View templates
const views = {
home: () => `
<div class="hero">
<h2>Welcome to Task Manager</h2>
<p>Manage your tasks efficiently and stay productive!</p>
<a href="#/tasks" class="btn" data-link>View Tasks</a>
</div>
`,
tasks: () => `
<h2>My Tasks</h2>
<div id="task-list">
<div class="task">
<input type="checkbox">
<span>Complete JavaScript tutorial</span>
</div>
<div class="task">
<input type="checkbox">
<span>Build a SPA application</span>
</div>
<div class="task">
<input type="checkbox">
<span>Learn about routing</span>
</div>
</div>
`,
about: () => `
<h2>About Task Manager</h2>
<p>Task Manager is a simple application to help you organize your daily tasks.</p>
<p>Built with HTML, CSS, and JavaScript.</p>
`,
notFound: () => `
<div class="hero">
<h2>404 - Page Not Found</h2>
<p>The page you're looking for doesn't exist.</p>
<a href="#/" class="btn" data-link>Go Home</a>
</div>
`
};
// Router logic
function router() {
// Get the hash (e.g., #/tasks)
const hash = window.location.hash.slice(1) || '/';
// Map hash to view
let view;
if (hash === '/') {
view = views.home();
} else if (hash === '/tasks') {
view = views.tasks();
} else if (hash === '/about') {
view = views.about();
} else {
view = views.notFound();
}
// Update the app container
document.getElementById('app').innerHTML = view;
// Update active nav link
updateActiveLink(hash);
}
// Update active navigation link
function updateActiveLink(currentHash) {
document.querySelectorAll('.nav-links a').forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === `#${currentHash}`) {
link.classList.add('active');
}
});
}
// Listen for hash changes
window.addEventListener('hashchange', router);
// Initial load
router();
Step 3: Test Your SPA
- Open
spa-index.htmlin your browser - Click the navigation links
- Notice: No page reload! Content changes instantly
- Notice: URL changes in the address bar (hash-based routing)
- Notice: Browser back/forward buttons work
Key Observation: Navigation is instant with no page reload!
Limitations of This Simple SPA
- Uses hash-based routing (
#/page) instead of clean URLs - No support for route parameters (e.g.,
/tasks/123) - Manual route management is tedious
- No nested routes or route guards
Solution: Use a router library like Navigo!
Part 5: Introduction to Navigo
Navigo is a simple, lightweight JavaScript router for SPAs. It provides:
- Clean URL routing (no hash required)
- Route parameters and wildcards
- Route hooks (before/after navigation)
- TypeScript support
- Small bundle size (~3KB)
Installing Navigo
Add this to your HTML <head>:
<script src="https://unpkg.com/navigo@8"></script>
Part 6: Build an SPA with Navigo
Let's rebuild our SPA using Navigo with clean URLs and advanced routing features.
Step 1: Create the HTML
Create navigo-index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPA intro</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<nav class="navbar">
<div class="container">
<h1>Task Manager</h1>
<ul class="nav-links">
<li><a href="/" data-navigo>Home</a></li>
<li><a href="/tasks" data-navigo>Tasks</a></li>
<li><a href="/about" data-navigo>About</a></li>
</ul>
</div>
</nav>
<main id="app" class="container">
<!-- Content will be dynamically loaded here -->
</main>
<footer class="footer">
<p>© 2026 Task Manager. All rights reserved.</p>
</footer>
<!-- Navigo CDN -->
<script src="https://unpkg.com/navigo@8"></script>
<script type="module" src="navigo-app.js"></script>
</body>
</html>
Step 2: Create the Router
Create navigo-app.js:
// navigo-app.js
// Initialize Navigo router
// Use '/' as root if hosting at domain root, or '/your-app' if in a subfolder
const router = new Navigo('/');
// Get the app container
const app = document.getElementById('app');
// View rendering functions
function renderHome() {
app.innerHTML = `
<div class="hero">
<h2>Welcome to Task Manager</h2>
<p>Manage your tasks efficiently and stay productive!</p>
<a href="/tasks" data-navigo class="btn">View Tasks</a>
</div>
`;
updateActiveLink('/');
}
function renderTasks() {
app.innerHTML = `
<h2>My Tasks</h2>
<div id="task-list">
<div class="task">
<input type="checkbox">
<span>Complete JavaScript tutorial</span>
</div>
<div class="task">
<input type="checkbox">
<span>Build a SPA application</span>
</div>
<div class="task">
<input type="checkbox">
<span>Learn about routing</span>
</div>
</div>
<div style="margin-top: 2rem;">
<a href="/tasks/1" data-navigo class="btn">View Task 1</a>
<a href="/tasks/2" data-navigo class="btn">View Task 2</a>
</div>
`;
updateActiveLink('/tasks');
}
function renderTaskDetail(params) {
const taskId = params.data.id;
app.innerHTML = `
<h2>Task Details</h2>
<div class="task" style="font-size: 1.2rem;">
<p><strong>Task ID:</strong> ${taskId}</p>
<p><strong>Title:</strong> Task ${taskId}</p>
<p><strong>Status:</strong> In Progress</p>
</div>
<a href="/tasks" data-navigo class="btn">Back to Tasks</a>
`;
updateActiveLink('/tasks');
}
function renderAbout() {
app.innerHTML = `
<h2>About Task Manager</h2>
<p>Task Manager is a simple application to help you organize your daily tasks.</p>
<p>Built with HTML, CSS, JavaScript, and Navigo for routing.</p>
`;
updateActiveLink('/about');
}
function render404() {
app.innerHTML = `
<div class="hero">
<h2>404 - Page Not Found</h2>
<p>The page you're looking for doesn't exist.</p>
<a href="/" data-navigo class="btn">Go Home</a>
</div>
`;
}
// Update active navigation link
function updateActiveLink(path) {
document.querySelectorAll('.nav-links a').forEach(link => {
link.classList.remove('active');
const href = link.getAttribute('href');
if (href === path || (path.startsWith(href) && href !== '/')) {
link.classList.add('active');
}
});
}
// Define routes
router
.on('/', renderHome)
.on('/tasks', renderTasks)
.on('/tasks/:id', renderTaskDetail) // Route with parameter
.on('/about', renderAbout)
.notFound(render404);
// Resolve the initial route
router.resolve();
Step 3: Test Navigo Routing
Understanding the Code
Navigo Initialization:
const router = new Navigo('/');
- Creates a router instance
/means app is hosted at the domain root
Route Definition:
router
.on('/', renderHome) // Exact match
.on('/tasks', renderTasks) // Exact match
.on('/tasks/:id', renderTaskDetail) // Parameter route
.notFound(render404); // Fallback
Route Parameters:
function renderTaskDetail(params) {
const taskId = params.data.id; // Access the :id parameter
// Use taskId to fetch and display task
}
The data-navigo Attribute:
<a href="/tasks" data-navigo>Tasks</a>
- Tells Navigo to intercept this link
- Prevents full page reload
- Handles navigation via router
Part 7: Advanced Navigo Features
Route Parameters
// Single parameter
router.on('/users/:id', (params) => {
console.log(params.data.id); // "123" from /users/123
});
// Multiple parameters
router.on('/posts/:year/:month/:slug', (params) => {
console.log(params.data.year); // "2026"
console.log(params.data.month); // "02"
console.log(params.data.slug); // "my-post"
});
Query Parameters
router.on('/search', (params) => {
console.log(params.params); // { q: "javascript", type: "tutorial" }
// From URL: /search?q=javascript&type=tutorial
});
Programmatic Navigation
// Navigate to a route
router.navigate('/tasks');
// Navigate with parameters
router.navigate('/tasks/42');
// Navigate with query params
router.navigate('/search', { q: 'javascript' });
// Go back
window.history.back();
Named Routes
router
.on('/tasks/:id', renderTaskDetail, 'task-detail')
.resolve();
// Navigate by name
router.navigate('task-detail', { id: 42 });