Managing Base URLs in Web Applications - Sweet Skills #2

Hi Everyone,

I hope we had a great week. I spent mine working on really buggy code and I'll share a few tips I have discovered can help prevent us from giving another software developer a migraine when working on our web projects.

If this happens much longer, I'll go bald

I present to you:

The Base URL problem

If you work on a web application, you may have come across this issue, when your browser scripts make API calls to servers existing at various addresses:

$http.get('https://hacker-news.firebaseio.com/v0/item/8863.json?print=pretty').then((response) => {
  ... do stuff with response ...
})

PS: The $http module exists in the AngularJS Framework (think fetch API)

or images that exist at various CDN servers, and have their paths resolved by javascript:

let imgElem = document.createElement("img")  
imgElem.src = 'https://unsplash.it/200/300'  

You'll have heard people say stuff like "you shouldn't hard-code such paths in your application", and they're generally right ... repeating such URLs present a huge problem when the base addresses for the URLs have to change.

An example could be if we don't want to use the v0 version of the hacker-news API at https://hacker-news.firebaseio.com/v0/ anymore and we want to switch to a v1 version at https://hacker-news.firebaseio.com/v1/

We would have to find and modify every instance of the base URL in our application (perhaps, using project-wide find-and-replace?) 😏 ... See why it's a bad idea?

PS: If you still don't fully understand why repeating yourself in code is a problem, read about the DRY principle in software engineering.

The Proposed Solution(s)

TL;DR; I recommend the Base HREF Solution 😋

Store the Base URLs in constants

So, we can't repeat our base URLs anymore ... we'd have to store it in a variable somewhere and use, yea?

Let's see how that'll work out:

const hackerNewsApiBaseUrl = 'https://hacker-news.firebaseio.com/v0/'  
//usage
$http.get(hackerNewsApiBaseUrl + 'item/8863.json?print=pretty').then((response) => {
  ... do stuff with response ...
})

This could work in a perfect world.

However, my country's president is still missing and nothing has been done about it 😢.

If you see this man, please return to Nigerians ... We miss him!

What if we wanted to change how the relative path is resolved for some reason (which is a thing that could happen)? This brings us to ...

Use a Function to resolve the path

We can modify our code to use a function that takes the relative path as an argument and returns the full URL. Something like:

let hackerNewsApiBaseUrl = (url) => 'https://hacker-news.firebaseio.com/v0/' + url  
//usage
$http.get(hackerNewsApiBaseUrl('item/8863.json?print=pretty')).then((response) => {
  ... do stuff with response ...
})

Now, we have flexibility and can manipulate the relative paths scope-wide just by changing the resolver function.

We have attained the flexibility skill ...

But will we do this for every resolved URL? What if there exists different root paths for different pages? What will we do? Page-specific JavaScript files containing the different resolver functions come to mind, but I shall suggest an alternative:

Base HREF Attributes

I learned about the <base href="path"> element when learning to build single page applications with AngularJS. It is a way to "Set any URL you choose as the base for all relative URLs", we'll see why this is very promising.

With the <base> tag, we can make sure that all our relative paths on our page have a common base. It affects <a>, <img>, <link> tags and also affects ajax requests as long as they make requests to relative URLs. See this article to read more about how the <base> element works.

We can overload the <base> element and use that to solve our problems.

<base href="/">  
<base href="https://hacker-news.firebaseio.com/v0/" name="hackerNewsApiBaseUrl">  
//url-resolver-functions.js
let rootUrl = (url, nameVal) => {  
    const bases = Array.from(document.getElementsByTagName("base"));
    const baseElem = bases.filter((elem) => (elem.getAttribute("name") == (nameVal || "root")))[0] || bases.filter((elem) => !elem.getAttribute("name"))[0];
    if (baseElem) {
        var ret = baseElem.getAttribute("href");
        if (ret.charAt(ret.length - 1) != "/") ret += "/";
        ret += url;
        return ret;
    }
    else return "/" + url;
}

let hackerNewsApiBaseUrl = (url) => rootUrl(url, "hackerNewsApiBaseUrl")  

Woah, chill ... don't run yet!

What this means is that by overloading the <base> element in the <head> of our pages, we can specify different base URLs for the many resources on our pages, while needing only one script file to manage the resolver functions.

Some of your pages can have the base URL of the hacker news API set to v0 version with:

<base href="https://hacker-news.firebaseio.com/v0/" name="hackerNewsApiBaseUrl">  

while others can use the v1 version using:

<base href="https://hacker-news.firebaseio.com/v1/" name="hackerNewsApiBaseUrl">  

What does this mean for our code?

It means we can finally stop hard-coding our URLs 🙌. No, really ... please let's stop 😢

This also gives us a clean way to handle our base URLs. If you work with a server-side MVC framework like ASP.NET or Laravel, you should find this really easy to work with.

In a single-page-application, you may not readily get the advantage of being able to change the value of the <base> element href values on different pages, without some really cool tweaking. If I every have to solve that problem, you bet there'll be another post about it. 😁

If you have enjoyed this, please recommend ❤ this article by clicking the heart below. Also, if you have any questions or suggestions, please use the comments section below.

Till next time, Enjoy!

Show Comments