{"id":10528,"date":"2025-10-22T13:32:54","date_gmt":"2025-10-22T13:32:53","guid":{"rendered":"https:\/\/namastedev.com\/blog\/?p=10528"},"modified":"2025-10-22T13:32:54","modified_gmt":"2025-10-22T13:32:53","slug":"routing-without-a-framework-building-a-minimal-spa-router","status":"publish","type":"post","link":"https:\/\/namastedev.com\/blog\/routing-without-a-framework-building-a-minimal-spa-router\/","title":{"rendered":"Routing Without a Framework: Building a Minimal SPA Router"},"content":{"rendered":"<h1>Routing Without a Framework: Building a Minimal SPA Router<\/h1>\n<p>Single Page Applications (SPAs) have become the go-to architecture for modern web applications, offering a seamless user experience by loading content dynamically without refreshing the entire page. However, many developers rely on heavy frameworks that may be overkill for smaller projects. In this blog, we\u2019ll explore how to build a minimal router from scratch, achieving efficient navigation in an SPA without the need for a large framework.<\/p>\n<h2>Understanding the Basics of SPA Routing<\/h2>\n<p>Before diving into creating a router, it&#8217;s crucial to understand what routing is and how it works in the context of an SPA. Routing refers to the mechanism that allows users to navigate between different views or components within an application. In SPAs, this is usually achieved through client-side manipulation of the browser&#8217;s history API.<\/p>\n<p>When a user clicks on a link, the browser URL updates, and the associated content for that route is fetched and displayed without a full page reload. This enhances user experience and reduces loading times significantly.<\/p>\n<h2>Setting Up Your Project<\/h2>\n<p>To get started, we\u2019ll set up a simple HTML file and a JavaScript file. Here is a basic structure:<\/p>\n<pre><code>&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n  &lt;head&gt;\n    &lt;title&gt;My Minimal SPA&lt;\/title&gt;\n    &lt;style&gt;\n      body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }\n      a { text-decoration: none; color: blue; }\n      .content { margin-top: 20px; }\n    &lt;\/style&gt;\n  &lt;\/head&gt;\n  &lt;body&gt;\n    &lt;nav&gt;\n      &lt;a href=\"\/\" data-link&gt;Home&lt;\/a&gt; |\n      &lt;a href=\"\/about\" data-link&gt;About&lt;\/a&gt; |\n      &lt;a href=\"\/contact\" data-link&gt;Contact&lt;\/a&gt;\n    &lt;\/nav&gt;\n    &lt;div class=\"content\"&gt;&lt;\/div&gt;\n    \n    &lt;script src=\"app.js\"&gt;&lt;\/script&gt;\n  &lt;\/body&gt;\n&lt;\/html&gt;\n<\/code><\/pre>\n<p>Create an HTML file as outlined, and ensure you have a JavaScript file named <strong>app.js<\/strong>.<\/p>\n<h2>Creating the Router<\/h2>\n<p>Now, let\u2019s write our basic router in JavaScript. The router&#8217;s responsibility will be to handle navigation, render the appropriate content based on the URL, and manipulate the browser&#8217;s history.<\/p>\n<pre><code>const routes = {\n  '\/': '<h2>Home Page<\/h2>&lt;p&gt;Welcome to our minimal SPA!&lt;\/p&gt;',\n  '\/about': '&lt;h2&gt;About Us&lt;\/h2&gt;&lt;p&gt;We are a small team of developers.&lt;\/p&gt;',\n  '\/contact': '&lt;h2&gt;Contact Us&lt;\/h2&gt;&lt;p&gt;You can reach us at contact@example.com.&lt;\/p&gt;',\n};\n\nfunction router() {\n  const content = document.querySelector('.content');\n  const path = window.location.pathname;\n  \n  content.innerHTML = routes[path] || '&lt;h2&gt;404 Not Found&lt;\/h2&gt;';\n}\n\nwindow.addEventListener('popstate', router);\n\ndocument.querySelectorAll('[data-link]').forEach(link =&gt; {\n  link.addEventListener('click', function(e) {\n    e.preventDefault();\n    const path = this.getAttribute('href');\n    window.history.pushState(null, '', path);\n    router();\n  });\n});\n\n\/\/ Initial load\nrouter();\n<\/code><\/pre>\n<h3>Code Explanation<\/h3>\n<p>Let\u2019s break down the code we just wrote:<\/p>\n<ul>\n<li><strong>Routes Definition:<\/strong> We created an object called <strong>routes<\/strong> that maps URL paths to HTML content. This object serves as a simple lookup table.<\/li>\n<li><strong>Router Function:<\/strong> The <strong>router<\/strong> function retrieves the current path from the browser&#8217;s URL and updates the inner HTML of the content div based on the defined routes, or a 404 message if the path doesn\u2019t exist.<\/li>\n<li><strong>Popstate Event:<\/strong> This event triggers the router function when the user navigates back or forward in their browser history, ensuring the app responds appropriately.<\/li>\n<li><strong>Link Click Handler:<\/strong> Each link with the <strong>data-link<\/strong> attribute has a click event listener. It prevents the default link behavior, updates the browser&#8217;s history state using <strong>pushState<\/strong>, and calls the router function to render new content.<\/li>\n<\/ul>\n<h2>Adding a 404 Not Found Page<\/h2>\n<p>It\u2019s a good practice to provide feedback when an invalid path is requested. We have already added a basic 404 message in our router function. Still, we can enhance it further by creating a dedicated 404 component.<\/p>\n<pre><code>const routes = {\n  '\/': '&lt;h2&gt;Home Page&lt;\/h2&gt;&lt;p&gt;Welcome to our minimal SPA!&lt;\/p&gt;',\n  '\/about': '&lt;h2&gt;About Us&lt;\/h2&gt;&lt;p&gt;We are a small team of developers.&lt;\/p&gt;',\n  '\/contact': '&lt;h2&gt;Contact Us&lt;\/h2&gt;&lt;p&gt;You can reach us at contact@example.com.&lt;\/p&gt;',\n  '404': '&lt;h2&gt;404 Not Found&lt;\/h2&gt;&lt;p&gt;Oops! The page you are looking for does not exist.&lt;\/p&gt;'\n};\n\nfunction router() {\n  const content = document.querySelector('.content');\n  const path = window.location.pathname;\n\n  if (routes[path]) {\n    content.innerHTML = routes[path];\n  } else {\n    content.innerHTML = routes['404'];\n  }\n}\n<\/code><\/pre>\n<h2>Enhancing the Router with Dynamic Paths<\/h2>\n<p>Our current router only handles static paths. To make it more versatile, we can support dynamic paths that allow for parameters, such as user IDs or product IDs.<\/p>\n<pre><code>const routes = {\n  '\/': '&lt;h2&gt;Home&lt;\/h2&gt;',\n  '\/user\/:id': (id) =&gt; `&lt;h2&gt;User Profile&lt;\/h2&gt;&lt;p&gt;User ID: ${id}&lt;\/p&gt;`,\n};\n\nfunction matchRoute(path) {\n  const routeKeys = Object.keys(routes);\n  for (const route of routeKeys) {\n    const regex = new RegExp(route.replace(\/:id\/, '(\\w+)')); \/\/ Matches dynamic id\n    const match = path.match(regex);\n    if (match) {\n      return typeof routes[route] === 'function' ? routes[route](match[1]) : routes[route];\n    }\n  }\n  return routes['404'];\n}\n\nfunction router() {\n  const path = window.location.pathname;\n  const content = document.querySelector('.content');\n  content.innerHTML = matchRoute(path);\n}\n<\/code><\/pre>\n<h3>Dynamic Path Breakdown<\/h3>\n<p>In this updated example, we added a new route that includes a dynamic parameter, <strong>:id<\/strong>. The <strong>matchRoute<\/strong> function uses a regular expression to check if the requested path matches any of the defined routes. If a match is found, and if that route is a function, it gets called with the captured parameter.<\/p>\n<h2>Handling Scroll Restoration<\/h2>\n<p>By default, the SPA does not preserve the scroll position when navigating through different routes, which can be problematic. To enhance user experience, we should save and restore scroll positions.<\/p>\n<pre><code>const scrollPositions = {};\n\nfunction router() {\n  const path = window.location.pathname;\n  const content = document.querySelector('.content');\n  scrollPositions[path] = window.scrollY; \/\/ Save the scroll position of the current path\n  content.innerHTML = matchRoute(path);\n  \n  \/\/ Restore scroll position on navigation\n  window.scrollTo(0, scrollPositions[path] || 0);\n}\n<\/code><\/pre>\n<h2>Styling and User Experience<\/h2>\n<p>In addition to functionality, the visual appeal and user experience of your SPA are paramount. Consider adding CSS classes to your content for transitions and animations, enhancing the perceived performance and responsiveness of the application.<\/p>\n<pre><code>&lt;style&gt;\n  .fade-in { opacity: 0; transition: opacity 0.5s ease-in; }\n  .fade-in.active { opacity: 1; }\n&lt;\/style&gt;\n\nconst router = () =&gt; {\n  const path = window.location.pathname;\n  const content = document.querySelector('.content');\n  content.innerHTML = matchRoute(path);\n  content.classList.add('fade-in', 'active');\n}\n<\/code><\/pre>\n<h2>Conclusion<\/h2>\n<p>Creating a minimal SPA router from scratch allows developers to have finer control over the routing logic without the complexities of full-fledged frameworks. This lightweight approach enables better performance, easier debugging, and a customizable user experience.<\/p>\n<p>By following the steps outlined in this blog, you now have a solid foundation for a minimal SPA router. You can expand it further by adding features like guards for authentication, nested routes, and even an integrated state management solution.<\/p>\n<p>Building your routing system can be an enlightening experience, helping you understand the underlying mechanics of web applications and enhancing your skills as a developer. So why not give it a try?<\/p>\n<p>Happy coding!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Routing Without a Framework: Building a Minimal SPA Router Single Page Applications (SPAs) have become the go-to architecture for modern web applications, offering a seamless user experience by loading content dynamically without refreshing the entire page. However, many developers rely on heavy frameworks that may be overkill for smaller projects. In this blog, we\u2019ll explore<\/p>\n","protected":false},"author":231,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[203],"tags":[898,899,814],"class_list":{"0":"post-10528","1":"post","2":"type-post","3":"status-publish","4":"format-standard","6":"category-web-development","7":"tag-navigation","8":"tag-spa","9":"tag-web-technologies"},"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/namastedev.com\/blog\/wp-json\/wp\/v2\/posts\/10528","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/namastedev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/namastedev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/namastedev.com\/blog\/wp-json\/wp\/v2\/users\/231"}],"replies":[{"embeddable":true,"href":"https:\/\/namastedev.com\/blog\/wp-json\/wp\/v2\/comments?post=10528"}],"version-history":[{"count":1,"href":"https:\/\/namastedev.com\/blog\/wp-json\/wp\/v2\/posts\/10528\/revisions"}],"predecessor-version":[{"id":10529,"href":"https:\/\/namastedev.com\/blog\/wp-json\/wp\/v2\/posts\/10528\/revisions\/10529"}],"wp:attachment":[{"href":"https:\/\/namastedev.com\/blog\/wp-json\/wp\/v2\/media?parent=10528"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/namastedev.com\/blog\/wp-json\/wp\/v2\/categories?post=10528"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/namastedev.com\/blog\/wp-json\/wp\/v2\/tags?post=10528"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}