Add Client Side Routing in ASP.NET Core

As an ASP.NET Core developer you are familiar with server side routing. If you ever worked with JavaScript frameworks such as Angular you might be familiar with their client side routing capabilities. You are probably aware that many frameworks and libraries offer a client side "router" that enables client side routing capabilities for your JavaScript code. For real-world professional web apps it makes sense to pick some robust and feature rich client side routing mechanism. But for simple purposes and for learning how client side routing works, it would be good to write some code that implements such a capabilities. To that end this article illustrates how a simple client side router can be built using JavaScript.

Take a look at the following HTML page rendered in the browser.

The page consists of three buttons - Hello World, Hello Galaxy, and Hello Universe. Initially when the page is loaded, a Welcome message is displayed to the user. Upon clicking on the buttons respective message is displayed. Additionally, the URL shown in the address bar changes to indicate the button being clicked. These are client side routes.

The figure also shows client side URL being bookmarked by the end user. If you hit the bookmarked URL in a new browser session, it will throw an error because the server doesn't have any idea about the client side routes. You will also learn to fix this error using ASP.NET Core code.

Let's get going!

To begin, create a new ASP.NET Core MVC web application using Visual Studio.

Right click on the wwwroot folder and add a new HTML page named Index.html

Open the index.html file and add a <script> reference to jQuery library in the head section.

<script src="/lib/jquery/dist/jquery.js"></script>

We will use jQuery to handle the click events of those three buttons. If you want you can skip jQuery altogether and use plain JavaScript to accomplish that task.

Then add a new JavaScript file named SimpleRouter.js in the wwwroot > js folder. This file will hold our custom routing code and is shown below:

Add a <script> reference to SimpleRouter.js like this:

<script src="/js/SimpleRouter.js"></script>

Now we will define client side routes required by our application. These routers are defined by an array of route objects and are added to the SimpleRouter.js as shown below:

const routes = [
        path: '/',
        template: '<h1>Welcome!</h1>'
        path: '/spa/world',
        template: '<h1>Hello World!</h1>'
        path: '/spa/galaxy',
        template: '<h1>Hello Galaxy!</h1>',
        path: '/spa/universe',
        template: '<h1>Hello Universe!</h1>',

As you can see, the routes array contains four route definitions. Each route definition consists of a path that holds a client side route under consideration. For example, /spa/world, /spa/galaxy, and /spa/universe are the client side routes required by our application. The first path / indicates the default route.

Each route object also holds an HTML template that will be rendered in the browser when the client side route is accessed. In this case templates simply output a message inside an <h1></h1> element.

Next, add a JavaScript class named SimpleRouter as shown below:

class SimpleRouter {


The JavaScript class keyword allows you to define classes. I won't go into too much details of JS classes in this article. You can read more about it here.

Then write the following code inside the SimpleRouter class.


constructor(routes) {
    this.routes = routes;

The code declares a field named routes. The routes field is assigned a value in the constructor of the class. Then the code grabs the current window location and passes it to handleRoute() method. Initially the pathname will be / but if you are clicking on a bookmarked URL it would be something like /spa/world (for example).

Ok. Now add the handleRoute() method inside the SimpleRouter class as shown below:

handleRoute(path) {
const route = this.findRoute(path);
window.history.pushState({}, '', path);
const templateContainer = 
templateContainer.innerHTML = route.template;

The above code calls the findRoute() method to determine which of the route objects from the routes array match the current route. You will write the findRoute() method shortly.

Then comes the important part. The code calls the pushState() method of the history object to programmatically add a history entry. The pushState() method is provided by HTML5 and most of the modern browsers support it.

The pushState() method takes three parameters. The first parameter represents a state object and we pass an empty object here because we don't intend to store any particular state information with this entry. The second parameter is the title of the new entry being added. This string title can be used to identity the entry. The third parameter is the client side URL and we pass the route path value in it. After the call to pushState() is made the browser's address bar reflects the new client side URL

Recollect that a route object contains two properties - path and template. The path property is used by the findRoute() method (discussed shortly). The template property is used here. The template property holds the HTML markup to be rendered in the browser when a particular route is navigated to. This template is rendered inside a <div> element with ID of container. We will see this <div> markup in detail later in this article.

The getElementById() method grabs the template container <div> element. Then we set its innerHTML property to the HTML template markup. This way browser's address bar shows a client side URL and the page also shows the associated templated.

The findRoute() method mentioned above is shown below. Add it to the SimpleRouter class.

findRoute(path) {
const testingFunc = function (routeItem) {
return (routeItem.path === path ? true : false);
const route = this.routes.find(testingFunc);
return route;

The findRoute() method basically finds a route object that matches a particular route path. It does so using the find() method of the routes array. The find() method expects a testing function that is used to find a particular array element matching certain criteria.

Therefore, the code declares a testingFunc function. The parameter to testing function is an individual routes object. Inside, the code checks if the supplied route path matches with a route item's path. If they match that route object is returned from the findRoute() method.

This completes SimpleRouter.js file.

Now add the following HTML markup inside the <body> of index.html file.

<button id="button1">Hello World!</button>
<button id="button2">Hello Galaxy!</button>
<button id="button3">Hello Universe!</button>

<div id="container"></div>

The above markup consists of three buttons and a <div> element. The three buttons navigate to a specific route (such as /spa/world, /spa/galaxy, and /spa/universe). The <div> element acts as the container for rendering the route template. To distinguish this <div> from other <div> elements we set its ID element to container. Recollect that this ID is used inside the handleRoute() method discussed earlier.

To handle click event of the three buttons you can write the following jQuery <script> block in the <head> section.

$(document).ready(function () {
    var router = new SimpleRouter(routes);

    $("#button1").click(function () {

    $("#button2").click(function () {

    $("#button3").click(function () {

As you can see, ready() handler creates a SimpleRouter object. The click event handlers of the three buttons call the handleRoute() method and pass the desired route path to it.

This completes the application. If you run this application at this stage it will try to hit /Home/Index action. But we don't want that to happen. We want that our Index.html page be loaded. To accomplish this, open the Startup class and add this line to the Configure() method.

public void Configure(IApplicationBuilder app, 
IWebHostEnvironment env)
    if (env.IsDevelopment())

The UseDefaultFiles() ensures that default documents from the wwwroot folder such as Index.html are loaded if present.

If you run the application you will see the outcome as shown below: 

Try clicking on various button and check whether address bar URL changes and also observe the template displayed in the browser.

So far so good.

Now try to do the following:

  • Go in the browser's address bar, manually type a valid client side URL (such as /spa/world) and hit enter.
  • When a client side URL is displayed in the browser bookmark the URL. Then click on the bookmarked URL.

You will find that any of the above actions don't result in expected outcome.

Why does this happen? That's because any of the above actions treat the URL to be a server side resource. And on the server there is no resource that corresponds to /spa/world (or any other client side URL).

To fix this error, you need to trap such client side URLs using ASP.NET Core's routing mechanism and map them to a valid controller action. So, add the following code to the Configure() method:

app.UseEndpoints(endpoints =>
        name: "default",
        pattern: "{controller=Home}/
        name: "infoPage",
        pattern: "spa/{*infoPage}",
        defaults: new
            controller = "Spa",
            action = "ShowIndexPage"

The above code sets a "catch-all" routes and hands them over to the ShowIndexPage() action of SpaController. You will add this controller and action next.

So, add a new MVC controller class named SpaController and aso add ShowIndexPage() action in it as shown below:

public class SpaController : Controller
    public IActionResult ShowIndexPage
([FromServices]IWebHostEnvironment env)
        return PhysicalFile
(env.WebRootPath + "/index.html", "text/html");

Observe t he ShowIndexPage() action carefully. It receives IWebHostEnvironment object through DI. Then it calls PhysicalFile() method to return a PhysicalFileResult object. The PhysicalFile() method accepts two parameters - physical path of a file to send as a response and file's MIME content type. In this case we want to return Index.html to the browser.

Now run the application again and try to invoke the bookmarked URL (or manually type it). This time control will go to ShowIndexPage() action and then Index.html is returned. Recollect that SimpleRouter's constructor implements logic to handle these initial URLs.

In this article we developed a very simple yet working client side router. You will find many professional and feature rich client side routers and plug-ins that you can use in your applications. Hopefully this example has given a backgrounder so that you get an idea about the internal working of such client side routers. 

That's it for now! Keep coding!!

Bipin Joshi is an independent software consultant, trainer, author, and meditation teacher. He has been programming, meditating, and teaching for 25+ years. He conducts instructor-led online training courses in ASP.NET family of technologies for individuals and small groups. He is a published author and has authored or co-authored books for Apress and Wrox press. Having embraced the Yoga way of life he also teaches Ajapa Yoga to interested individuals. To know more about him click here.

Get connected : Facebook  Twitter  LinkedIn  YouTube

Posted On : 09 March 2020

Tags : ASP.NET ASP.NET Core MVC C# JavaScript Visual Studio