Table of Contents
Get an introduction to web requests and the Fetch API to make sending and receiving data a breeze!
As you probably know very well by now, the internet is made up of a bunch of interconnected computers called servers. When you are surfing the web and navigating between web pages, what you are really doing is telling your browser to request information from any of these servers. It kinda looks as follows: your browser sends a request, waits awkwardly for the server to respond to the request, and (once the server responds) processes the request. All of this communication is made possible because of something known as the HTTP protocol.
The HTTP protocol provides a common language that allows your browser and a bunch of other things to communicate with all the servers that make up the internet. The requests your browser makes on your behalf using the HTTP protocol are known as HTTP requests, and these requests go well beyond simply loading a new page as you are navigating. A common (and whole lot more exciting!) set of use cases revolve around updating your existing page with data resulting from a HTTP request.
For example, you may have a page where you'd like to display some information about the currently logged-in user. This is information your page might not have initially, but it will be information your browser will request as part of you interacting with the page. The server will respond with the data and have your page update with that information. All of this probably sounds a bit abstract, so I'm going to go a bit weird for a few moments and describe what a HTTP request and response might look like for this example.
To get information about the user, here is our HTTP request:
GET /user
Accept: application/json
For that request, here is what the server might return:
200 OK
Content-Type: application/json
{
"name": "Kirupa",
"url": "https://www.kirupa.com"
}
This back and forth happens a bunch of times, and all of this is fully supported in JavaScript! This ability to asynchronously request and process data from a server without requiring a page navigation/reload has a term. That term is Ajax (or AJAX if you want to shout). This acronym stands for Asynchronous JavaScript and XML, and if you were around web developers a few years ago, Ajax was the buzzword everybody threw around for describing the kind of web apps we take for granted today - apps like Twitter, Facebook, Google Maps, Gmail, and more that constantly fetch data as you are interacting with the page without requiring a full page reload!
Knowing how to Ajax it up and make HTTP requests is a very important skill, and this tutorial will give you everything you need to be dangerous. Or, should I say...dangeresque?
Onwards!
Reading (or even thinking) about the HTTP and requests is boring...extremely boring! To help you both stay awake as well as understand what all is involved, we are going to be building a small example together. The example will look as follows:
On the surface, this example seems just as boring as the underlying details of an HTTP request that I was hoping to make seem more exciting. Like, what are you going to do with this knowledge about your IP?
What this example hides are some of the awesome underlying details relevant to what you are about to learn. Here is a sneak peek. We have some JavaScript that makes an HTTP request to a service (ipinfo.io) that returns a whole bunch of data about your connection. Using JavaScript, we process all that returned data and surgically pinpoint the IP address that we so proudly display here.
I don't know about you, but I'm totally excited to see this all come together. By the time you reach the end of this tutorial, you too will have created something similar to this example and learned all about what goes on under the hood to make it work.
The newest kid on the block for making HTTP requests is the fetch API. To use fetch in its most basic form, all we need to do is provide the URL to send our request to. Once the request has been made, a response will be returned that we can then process. To put all of these words into action, let's write some code and get our earlier example up and running.
If you want to follow along, create a new HTML document and add the following markup into it:
<!DOCTYPE html>
<html>
<head>
<title>Display IP Address</title>
</head>
<body>
<script>
</script>
</body>
</html>
Inside the script tag, add the following code that makes up our web request:
fetch("https://ipinfo.io/json")
.then(function (response) {
return response.json();
})
.then(function (myJson) {
console.log(myJson.ip);
})
.catch(function (error) {
console.log("Error: " + error);
});
Once you have added these lines, save your changes and test your page in the browser. You won't see anything displayed on screen, but if you bring up the Console via your browser developer tools, you should see your IP address getting displayed:
That's something! Now that we have our IP address getting displayed to our console, let's take a moment and revisit the code and see what exactly it is doing. With our first line of code, we are calling fetch and providing the URL we want to make our request to:
fetch("https://ipinfo.io/json")
.then(function (response) {
return response.json();
})
.then(function (myJson) {
console.log(myJson.ip);
})
.catch(function (error) {
console.log("Error: " + error);
});
The URL we send our request to is ipinfo.io/json. Once this line gets run, the service running on ipinfo.io will send us some data. It is up to us to process that data, and the following two then blocks are responsible for this processing:
fetch("https://ipinfo.io/json")
.then(function (response) {
return response.json();
})
.then(function (myJson) {
console.log(myJson.ip);
})
.catch(function (error) {
console.log("Error: " + error);
});
One really important detail to call out is that the response returned by fetch is a Promise. These then blocks are part of how promises work asynchronously to allow us to process the results. Covering promises goes beyond the scope of this article, but the MDN documentation does a great job explaining what they are. The thing to know for now is that we have a chain of then blocks where each block is called automatically after the previous one completes. Because this is all asynchronous, all of this is done while the rest of our app is doing its thing. We don’t have to do anything extra to ensure our request-related code doesn’t block or freeze up our app when waiting for a slow network result or processing a large amount of data.
Getting back to our code, in our first then block, we specify that we want the raw JSON data that our fetch call returns:
fetch("https://ipinfo.io/json")
.then(function (response) {
return response.json();
})
.then(function (myJson) {
console.log(myJson.ip);
})
.catch(function (error) {
console.log("Error: " + error);
});
In the next then block, which gets called after the previous one completes, we process the returned data further by narrowing in on the property that will give us the IP address and printing it to the console:
fetch("https://ipinfo.io/json")
.then(function (response) {
return response.json();
})
.then(function (myJson) {
console.log(myJson.ip);
})
.catch(function (error) {
console.log("Error: " + error);
});
How do we know that the IP address is going to be stored by the ip property from our returned JSON data? The easiest way is by referring to the ipinfo.io developer documentation! Every web service will have its own format for returning data when requested. It's up to us to take a few moments and figure out what the response will look like, what parameters we may need to pass in as part of the request to tune the response, and how we need to write our code to get the data that we want. As an alternative to reading the developer documentation, you can always inspect the response returned by the request via the developer tools. Use whichever approach is convenient for you.
We aren’t done with our code just yet. Sometimes the promise will result in an error or a failed response. When that happens, our promise will stop going down the chain of then blocks and look for a catch block instead. The code in this catch block will then execute. Our catch block looks as follows:
fetch("https://ipinfo.io/json")
.then(function (response) {
return response.json();
})
.then(function (myJson) {
console.log(myJson.ip);
})
.catch(function (error) {
console.log("Error: " + error);
});
We aren't do anything groundbreaking with our error handling. We just print the error message to the console.
What we have right now is a blank page with our IP address being printed to the console. Let's go ahead and add the few missing details to get our current page looking like the example page we saw at the beginning. In our current HTML document, make the following highlighted changes:
<!DOCTYPE html>
<html>
<head>
<title>Display IP Address</title>
<style>
body {
background-color: #FFCC00;
}
h1 {
font-family: sans-serif;
text-align: center;
padding-top: 140px;
font-size: 60px;
margin: -15px;
}
p {
font-family: sans-serif;
color: #907400;
text-align: center;
}
</style>
</head>
<body>
<h1 id=ipText></h1>
<p>( This is your IP address...probably :P )</p>
<script>
fetch("https://ipinfo.io/json")
.then(function (response) {
return response.json();
})
.then(function (myJson) {
document.querySelector("#ipText").innerHTML = myJson.ip;
})
.catch(function (error) {
console.log("Error: " + error);
});
</script>
</body>
</html>
The biggest changes here are adding some HTML elements to provide some visual structure and the CSS to make it all look good and proper. Notice that we also modified what our second then block does. Instead of printing our IP address to the console, we are instead displaying the IP address inside our ipText paragraph element.
If you preview your page now, you should see your IP address displayed in all its dark text and yellow backgrounded awesomeness.
The other (more traditional) object that is responsible for allowing you to send and receive HTTP requests is the weirdly named XMLHttpRequest. This object allows you to do several things that are important to making web requests. It allows you to:
There are a few more things that XMLHttpRequest does, and we'll cover them eventually. For now, these four will do just fine. Next, let's set the stage for re-creating the earlier example so that we can see all of this action for ourselves. In your existing HTML document from earlier, delete everything that is inside your script tag. Your document should look as follows:
<!DOCTYPE html>
<html>
<head>
<title>Display IP Address</title>
<style>
body {
background-color: #FFCC00;
}
h1 {
font-family: sans-serif;
text-align: center;
padding-top: 140px;
font-size: 60px;
margin: -15px;
}
p {
font-family: sans-serif;
color: #907400;
text-align: center;
}
</style>
</head>
<body>
<h1 id=ipText></h1>
<p>( This is your IP address...probably :P )</p>
<script>
</script>
</body>
</html>
With our document in a good state, it's time to build our example one line at a time!
The first thing we are going to do is initialize our XMLHttpRequest object, so add the following line inside your script tag:
let xhr = new XMLHttpRequest();
The xhr variable will now be the gateway to all the various properties and methods the XMLHttpRequest object provides for allowing us to make web requests. One such method is open. This method is what allows us to specify the details of the request we would like to make, so let's add it next:
let xhr = new XMLHttpRequest();
xhr.open('GET', "https://ipinfo.io/json", true);
The open method takes three-ish arguments:
So far, what we've done is initialized the XMLHttpRequest object and constructed our request. We haven't sent the request out yet, but that is handled by the next line:
let xhr = new XMLHttpRequest();
xhr.open('GET', "https://ipinfo.io/json", true);
xhr.send();
The send method is responsible for sending the request. If you set your request to be asynchronous (and why wouldn't you have?!!), the send method immediately returns and the rest of your code continues to run. That's the behavior we want.
When some code is running asynchronously, you have no idea when that code is going to return with some news. In the case of what we've done, once the HTTP request has been sent, our code doesn't stop and wait for the request to make its way back. Our code just keeps running. What we need is a way to send our request and then be notified of when the request comes back so that our code can finish what it started.
To satisfy that need, that's why we have events. More specifically for our case, that's why we have the readystatechange event that is fired by our XMLHttpRequest object whenever your request hits an important milestone on its epic journey.
To set this all up, go ahead and add the following highlighted line that invokes the almighty addEventListener:
let xhr = new XMLHttpRequest();
xhr.open('GET', "https://ipinfo.io/json", true);
xhr.send();
xhr.addEventListener("readystatechange", processRequest, false);
This line looks like any other event listening code you've written a bunch of times. We listen for the readystatechange event on our xhr object and call the processRequest event handler when the event gets overheard. Here is where some fun stuff happens!
This should be easy, right? We have our event listener all ready, and all we need is the processRequest event handler where we can add some code to read the result that gets returned. Let's go ahead and first add our event handler:
let xhr = new XMLHttpRequest();
xhr.open('GET', "https://ipinfo.io/json", true);
xhr.send();
xhr.onreadystatechange = processRequest;
function processRequest(e) {
}
Next, all we need is some code to parse the result of the HTTP request inside our newly added event handler...
As it turns out, it isn't that simple. The complication comes from the readystatechange event being tied to our XMLHttpRequest object's readyState property. This readyState property chronicles the path our HTTP request takes, and each change in its value results in the readystatechange event getting fired. What exactly is our readyState property representing that results in its value changing so frequently? Check out the following table:
Value | State | Description |
---|---|---|
0 | UNSENT | The open method hasn't been called yet |
1 | OPENED | The send method has been called |
2 | HEADERS_RECEIVED | The send method has been called and the HTTP request has returned the status and headers |
3 | LOADING | The HTTP request response is being downloaded |
4 | DONE | Everything has completed |
For every HTTP request that we make, our readyState property hits each of these five values. This means our readystatechange event gets fired five times. As a result, our processRequest event handler gets called five times as well. See where the problem is? For four out of the five times processRequest gets called, it won't be getting called for the reasons we are interested in - that is, the request has returned and it is time to analyze the returned data.
Since our goal is to read the returned value after our request has been completed, the readyState value of 4 is our friend. We need to ensure we only move forward when this value is set, so here is what the modified processRequest function would look like to handle that:
function processRequest(e) {
if (xhr.readyState == 4) {
// time to partay!!!
}
}
While this seems good, we have one more check to add. It is possible for us to find ourselves with no readable data despite our HTTP request having completed successfully. To guard against that, we also have HTTP status codes that get returned as a part of the request. You run into these HTTP status codes all the time. For example, whenever you see a 404, you know that a file is missing. You can see a full list of status codes if you are curious, but the one we care about with HTTP requests is status code 200. This code is returned by the server when the HTTP request was successful.
What we are going to do is modify our earlier code slightly to include a check for the 200 status code, and the appropriately named status property contains the value returned by the request:
function processRequest(e) {
if (xhr.readyState == 4 && xhr.status == 200) {
// time to partay!!!
}
}
In plain English, what this check does is simple. This if statement checks that the request has completed (readyState == 4) AND is successful (status == 200). Only if both of those conditions are met can we declare that our request did what we wanted it to do.
In the previous section, we looked at a whole lot of words for adding a simple if statement. I promise this section will be more to the point. All that is left is to read the body of the response that is returned. The way we do that is by reading the value of the responseText property returned by our xhr object:
function processRequest(e) {
if (xhr.readyState == 4 && xhr.status == 200) {
let response = JSON.parse(xhr.responseText);
document.querySelector("#ipText").innerHTML = response.ip;
}
}
Here is where things become a bit less general. When we make a request to the ipinfo.io server, the data gets returned in JSON form...as a string:
"{
"ip": "52.41.128.211",
"hostname": "static-52-41-128-211.blve.wa.verizon.net",
"city": "Redmond",
"region": "Washington",
"country": "US",
"loc": "46.6104,-121.1259",
"org": "Verizon, Inc",
"postal": "98052"
}"
To convert our JSON-like string into an actual JSON object, we pass in the result of xhr.responseText into the JSON.parse method. This takes our string of JSON data and turns it into an actual JSON object that is stored by the response variable. From there, displaying the IP is as easy as what is shown in the highlighted line:
function processRequest(e) {
if (xhr.readyState == 4 && xhr.status == 200) {
let response = JSON.parse(xhr.responseText);
document.querySelector("#ipText").innerHTML = response.ip;
}
}
I am not going to spend too much time on this section, for I don't want this to become a discussion of how the ipinfo.io server returns data. Just like what we saw with fetch earlier, every server we send a HTTP request to will send data in a slightly different way, and they may require you to jump through some slightly different hoops to get at what you are looking for. There isn't an easy solution that will prepare you for all of your future HTTP requesting needs outside of reading documentation for the web service you are interested in requesting data from.
Writing some code that makes an HTTP request and returns some data is probably one of the coolest things you can do in JavaScript. Everything you've seen here used to be a novelty that only Internet Explorer supported in the very beginning. Today, HTTP requests are everywhere. Much of the data that you see displayed in a typical page is often the result of a request getting made and processed - all without you even noticing. If you are building a new app or are modernizing an older app, the fetch API is a good one to start using if your app needs to make a web request. Since a good chunk of your time will be reading other people's code, there is a good chance that the web requests you encounter are made using XMLHttpRequest. In those cases, you need to know your way around. That's why this article focused on both the newer fetch and the older XMLHttpRequest approaches.
Just a final word before we wrap up. If you have a question and/or want to be part of a friendly, collaborative community of over 220k other developers like yourself, post on the forums for a quick response!
:: Copyright KIRUPA 2024 //--