How Single-Page Application Routing Works in the Bank Project

The bank project implements a vanilla-JavaScript single-page application router using the HTML5 History API to swap HTML templates dynamically without page reloads.

The banking demo in the microsoft/Web-Dev-For-Beginners repository demonstrates single-page application routing without React, Vue, or other frameworks. By combining the History API with native HTML <template> elements, the app renders distinct login and dashboard views while preserving standard browser navigation patterns. This lightweight approach reveals the fundamental mechanics underlying all client-side routers.

Core Routing Architecture

The SPA routes are defined in 7-bank-project/solution/app.js as a plain JavaScript object that maps URL paths to configuration objects. Each route specifies a page title, a template ID, and an optional initialization function.

// app.js (lines 20-24)
const routes = {
  '/dashboard': { title: 'My Account', templateId: 'dashboard', init: refresh },
  '/login':     { title: 'Login',      templateId: 'login',    init: attachAuthHandlers }
};

The HTML layer provides the mounting point and view templates in 7-bank-project/solution/index.html. The <div id="app"> serves as the active content container, while <template> elements contain dormant markup for each page.

<!-- index.html -->
<div id="app" aria-live="polite">Loading…</div>

<template id="login">
  <section class="login-page">…</section>
</template>

<template id="dashboard">
  <section class="dashboard-page">…</section>
</template>

The Routing Lifecycle

When a user clicks a navigation link or submits a form, the router executes a six-step sequence to update the URL and render the corresponding view.

1. Navigation Request

The navigate() function initiates route changes by pushing a state object onto the browser history stack and immediately invoking updateRoute().

// app.js (lines 25-29)
function navigate(path) {
  history.pushState({ path }, '', path);
  updateRoute();
}

2. Route Resolution

Inside updateRoute(), the router determines the current path by checking history.state.path first, then falling back to window.location.pathname. It looks up the matching configuration in the routes object, defaulting to /dashboard if the path is unrecognized.

// app.js (lines 31-34)
const path = history.state?.path || window.location.pathname;
const route = routes[path] || routes['/dashboard'];

3. Template Rendering

The function retrieves the relevant <template> element, clones its content, and injects the fragment into the DOM mount point. This approach avoids string-based HTML injection by using the content property of <template> elements.

// app.js (lines 35-39)
const template = document.getElementById(route.templateId);
const view = template.content.cloneNode(true);
const app = document.getElementById('app');
app.innerHTML = '';
app.appendChild(view);

4. Post-Render Wiring

After DOM insertion, attachGlobalHandlers() binds event listeners for logout buttons and other global UI elements. If the route configuration includes an init function (such as refresh for the dashboard or attachAuthHandlers for login), the router executes it asynchronously.

// app.js (lines 41-46)
attachGlobalHandlers();
if (typeof route.init === 'function') {
  Promise.resolve(route.init()).catch(err => console.error(err));
}

5. Browser Navigation Events

To support back and forward button usage, the code listens for popstate events. When the user navigates through history, the event triggers updateRoute() to synchronize the UI with the current URL.

// app.js (lines 50-52)
window.addEventListener('popstate', () => updateRoute());

6. Initial Application Load

The init() function bootstraps the application on page load. It checks existing authentication state to determine the starting path—/dashboard for logged-in users or /login for anonymous visitors—and seeds the history stack using replaceState() to avoid creating duplicate entries.

// app.js (lines 96-103)
if (!history.state || !history.state.path) {
  const initialPath = state.account ? '/dashboard' : '/login';
  history.replaceState({ path: initialPath }, '', initialPath);
}
updateRoute();

Extending the Router

Adding new pages requires only a template definition and a route entry. No build step or dependency installation is necessary.

To add a static About page:

  1. Define the route in app.js:
const routes = {
  '/dashboard': { title: 'My Account', templateId: 'dashboard', init: refresh },
  '/login':     { title: 'Login',      templateId: 'login',    init: attachAuthHandlers },
  '/about':     { title: 'About Us',   templateId: 'about' }
};
  1. Create the template in index.html:
<template id="about">
  <section class="page">
    <h1>About Squirrel Banking</h1>
    <p>This demo showcases single-page application routing with vanilla JavaScript.</p>
  </section>
</template>

Programmatic navigation anywhere in the codebase simply requires calling navigate():

// Redirect to dashboard after successful API login
navigate('/dashboard');

Summary

  • The bank project uses a route table in app.js (lines 20-24) to map URL paths to HTML template IDs and initialization functions.
  • History API methods (pushState, replaceState) update the URL without server requests, while popstate listeners (lines 50-52) handle browser back/forward buttons.
  • The updateRoute() function (lines 31-46) resolves paths, clones <template> content, and mounts fragments into #app.
  • Route-specific logic executes through optional init callbacks defined in the route configuration object.

Frequently Asked Questions

How does this vanilla JavaScript router differ from React Router or Vue Router?

Unlike framework-specific routers that use virtual DOM diffing, this implementation manipulates the actual DOM directly by cloning HTML <template> elements. It lacks features like nested routes or lazy loading, but demonstrates the core History API mechanics that power all SPA routers. The Microsoft Web-Dev-For-Beginners project intentionally uses this minimal pattern to teach routing fundamentals without framework abstractions.

Why does the router use HTML template elements instead of innerHTML strings?

The <template> element provides a content DocumentFragment that can be cloned and inserted without parsing HTML strings or exposing the application to XSS vulnerabilities via innerHTML assignment. This approach keeps markup declarative in the HTML file while JavaScript handles the logic, separating concerns more cleanly than string concatenation.

How does the router handle direct URL access or page refreshes?

On initial load, the init() function (lines 96-103) checks history.state and uses history.replaceState() to establish the correct entry point based on authentication status. However, this specific implementation requires a static server configuration that serves index.html for all routes, as the server-side routing is not handled by this client-side code alone.

Can this routing pattern scale to larger applications?

While functional for demos, this pattern lacks features required for complex applications: route guards, code splitting, nested layouts, and error boundaries. The route table is a flat object that could grow unwieldy, and global event handlers require manual cleanup to prevent memory leaks. For production software, migrating to a framework-specific router or a library like navigo or page.js is recommended.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →