Create a custom fetch API from XMLHttpRequest

Photo by Rob Fuller on Unsplash

What is your worst nightmare?

That sounded dark, but it’s not a rhetorical question, I really want to know because I am about to tell you mine and along the way we will learn some things like how the fetch API works and also how function constructors work.

Sorry I digress, back to my worst nightmare. If you asked me that question last week (at the time of reading this) it would be the below list in no particular order:

  • Writing Pre-ES6 syntax
  • No fetch API
  • No Transpiler (Babel/Typesccript)
  • Uncle Bob saying that I’m a dissapointment (Kidding)

If your list matches mine then I have to say that you are a very wierd person. As luck would have it I was called to work on a project that brought to life my nightmare list(excluding the last one), I was to add a new feature to the application. It was a legacy codebase that used purely pre-es6 syntax and XMLHttpRequest (the horror) for it’s AJAX requests.

So in a bid to make the experience palatable I decided to create a function that abstracts all the AJAX requests I will be making and expose APIs that mimics the new fetch API (Well not really). This is also after I watched the Javascript: The new hard parts video on frontend masters where an amazing explanation of how the fetch API works uder the hood was given. Let’s begin.

First, I had to look up how XMLHttpRequest works (obviously), then I started writing the function. My first iteration looked like this:

"use strict";
function fetch() {
var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var xhr = new XMLHttpRequest();
var onFufillment = [];
var onError = [];
var onCompletion = [];
var method = "GET" || options.method;
xhr.onreadystatechange = function () {
var _data = this;
if (this.readyState == 4 && this.status == 200) {
// Action to be performed when the document is read;
onFufillment.forEach(function (callback) {
callback(_data);
});
     onCompletion.forEach(function (callback) {
callback(_data);
});
} else if (this.readyState == 4 && this.status !== 200) {
onError.forEach(function (callback) {
callback(_data);
});
onCompletion.forEach(function (callback) {
callback(_data);
});
}
};
xhr.open(method, url, true);
xhr.send();
return {
then: function then(fufillmentFunction) {
onFufillment.push(fufillmentFunction);
},
catch: function _catch(errorFunction) {
onError.push(errorFunction);
},
finally: function _finally(completionFunction) {
onCompletion.push(completionFunction);
}
};
}

Let me work through what the function does:

  • We are checking if the url argument is passed into the function and then defaulting to an empty string if nothing is passed
  • We are also doing the same thing for the options argument and defaulting to an empty object if nothing is passed
  • Then we create a new instance of the XMLHttpRequest
  • We create 4 variables onFufillment, onError, onCompletion and method
  • onFufillment is anarray that stores all the functions passed into the then method
  • onError is an array that stores all the functions passed into the catch method
  • onCompletion is an array that stores all the functions passed into the finally method
  • method is used to store the HTTP method that will be used, it defaults to GET
  • We then pass a function into the onreadystatechange method of xhr which will be called when the state of the request changes
  • In the function, we save this into a _data variable so that it can be passed into the forEach functions without loosing it’s context (I know this is annoying)
  • We then check if the request is completed (readyState == 4 ) and if the request is successful, then we loop through onFufillment and onCompletion arrays, calling each function and passing _data into it
  • If the request fails we do the same thing with the onCompletion and onError arrays
  • Then we send of the request with the passed in parameters
  • After that, we return an object containing three functions, then. catch and finally which have the same names as the fetch API.
  • catch pushes the function that is passed as an argument into the onError array
  • then does the same thing with the onFufillment array
  • finally does the same with the onCompletion array

The usage of this API will look like this:

var futureData = fetch('https://jsonplaceholder.typicode.com/todos/2');
futureData.then(function(data){
console.log(data)
})
futureData.finally(function(response){
console.log(response);
});
futureData.catch(function(error){
console.log(error);
})

It works!!! But not nearly as the real fetch implementation. Can we do better than this? Of course we can. We can still add more features to the function e.g. we could make it chainable, that is, we can give it the ability to chain methods together.

On the second iteration, this is how it looks:

"use strict";
function fetch() {
var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var xhr = new XMLHttpRequest();
var onFufillment = [];
var onError = [];
var onCompletion = [];
var method = "GET" || options.method;
xhr.onreadystatechange = function () {
var _data = this;
if (this.readyState == 4 && this.status == 200) {
// Action to be performed when the document is read;
onFufillment.forEach(function (callback) {
callback(_data);
});
onCompletion.forEach(function (callback) {
callback(_data);
});
} else if (this.readyState == 4 && this.status !== 200) {
onError.forEach(function (callback) {
callback(_data);
});
onCompletion.forEach(function (callback) {
callback(_data);
});
}
};
xhr.open(method, url, true);
xhr.send();
return {
then: function then(fufillmentFunction) {
onFufillment.push(fufillmentFunction);
return this;
},
catch: function _catch(errorFunction) {
onError.push(errorFunction);
return this;
},
finally: function _finally(completionFunction) {
onCompletion.push(completionFunction);
return this;
}
};
}

The usage of the API will look like this:

var futureData = fetch('https://jsonplaceholder.typicode.com/todos/2');
futureData.then(function(data){
console.log(data)
}).then(function(response){
console.log(response);
}).catch(function(error){
console.log(error);
});

What did it do? The only difference in the second iteration was in the then, catch and finally where I just returned this which means each function returns itself basically enabling it to be chained (partially).

Better right? But can we do better than this? Of course we can. The returned object can be put in the functions prototype so that we can save memory in a situation where the function is used multiple times.

This is how it looks on the third iteration:

"use strict";
function fetch() {
var fetchMethod = Object.create(fetch.prototype);
var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var xhr = new XMLHttpRequest();
fetchMethod.onFufillment = [];
fetchMethod.onError = [];
fetchMethod.onCompletion = [];

var method = "GET" || options.method;
xhr.onreadystatechange = function () {
var _data = this;
if (this.readyState == 4 && this.status == 200) {
// Action to be performed when the document is read;
fetchMethod.onFufillment.forEach(function (callback) {
callback(_data);
});
fetchMethod.onCompletion.forEach(function (callback) {
callback(_data);
});
} else if (this.readyState == 4 && this.status !== 200) {
fetchMethod.onError.forEach(function (callback) {
callback(_data);
});
fetchMethod.onCompletion.forEach(function (callback) {
callback(_data);
});
}
};
xhr.open(method, url, true);
xhr.send();
return fetchMethod;
};
fetch.prototype.then = function(fufillmentFunction) {
this.onFufillment.push(fufillmentFunction);
return this;
};
fetch.prototype.catch = function(errorFunction) {
this.onError.push(errorFunction);
return this;
};
fetch.prototype.finally = function(completionFunction) {
this.onCompletion.push(completionFunction);
return this;
};

So this version basically moves the returned function into the fetch’s prototype, If you don’t understand the statement then I recommend checking out this article about Javascript’s prototype (Thanks, Tyler McGinnis).

Is this an improvement? Yes!!! Can we do better? Of course we can. We can use the new keyword to our advantage here and remove the explicit return statement.

The next iteration will look like this:

"use strict";
function Fetch() {
var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var xhr = new XMLHttpRequest();
this.onFufillment = [];
this.onError = [];
this.onCompletion = [];
var method = "GET" || options.method;
var internalFetchContext = this;
xhr.onreadystatechange = function () {
var _data = this;
if (this.readyState == 4 && this.status == 200) {
// Action to be performed when the document is read;
internalFetchContext.onFufillment.forEach(function (callback) {
callback(_data);
});
internalFetchContext.onCompletion.forEach(function (callback) {
callback(_data);
});
} else if (this.readyState == 4 && this.status !== 200) {
internalFetchContext.onError.forEach(function (callback) {
callback(_data);
});
internalFetchContext.onCompletion.forEach(function (callback) {
callback(_data);
});
}
};
xhr.open(method, url, true);
xhr.send();
};
Fetch.prototype.then = function(fufillmentFunction) {
this.onFufillment.push(fufillmentFunction);
return this;
};
Fetch.prototype.catch = function(errorFunction) {
this.onError.push(errorFunction);
return this;
};
Fetch.prototype.finally = function(completionFunction) {
this.onCompletion.push(completionFunction);
return this;
};

Let me explain the changes,

  • Changed the name of the function from fetch to Fetch, it’s just a convention when using the new keyword
  • Since I am using the new keyword I can then save the various arrays created to the this context.
  • Because the function passed into onreadystatechange has it’s own context I had to save the original this into it’s own variable to enable me call it in the function (I know, this can be annoying)
  • Converted the prototype functions to the new function name.

The usage will look like this:

var futureData = new Fetch('https://jsonplaceholder.typicode.com/todos/1');
futureData.then(function(data){
console.log(data)
}).then(function(response){
console.log(response);
}).catch(function(error){
console.log(error);
})

Voila!!! That was really fun. But can we do better? Of course we can.

But I will leave that to you. I would love to see your own implementation of the API in the comments below.

If you liked the article (Even if you didn’t), I would appreciate a clap (or 50) from you. Thank you.

Link to the codepen of the final iteration: