Lesson Weekend

Over the last several lessons, we've learned quite a bit about API calls. We've learned what they are and how to make an API call using Postman. We also took a more in-depth look at working with and parsing JSON. The skills we've covered so far are applicable for working with APIs no matter what programming language you use. Whether you are writing in Ruby, JavaScript, C#, or another language, there are tools for making and receiving requests and then parsing JSON or any other type of data the API returns.

Now we are ready to build out a JavaScript application that will make an API call. There are many different ways to make an API call in JavaScript ranging from using vanilla JavaScript to jQuery to using other tools that vastly simplify making API calls. We will use a vanilla JavaScript approach in this lesson. In later lessons, we will rewrite our code to make our API call in different ways. In the process, we'll also learn different tools for working with asynchronous code. After all, APIs are async. While we will only be learning how to apply async tools to API calls in this section, the async tools we learn can also be used for other asynchronous JavaScript operations as well.

However, we're not quite ready to jump off the async deep end just yet. In this lesson, we are making an API call the old-fashioned way - with just vanilla JavaScript. All the tools that jQuery uses to make API calls are based off this old-fashioned way. But we won't be covering the jQuery approach in this section because there's an even better approach called the Fetch API. Fetch is also built on top of this old-fashioned way. So while you may not be using this approach for this section's independent project (you can if you want!), you will have a better understanding of how tools like Fetch work when we actually use them later in this section.

Making an API Call in JavaScript

We will not include all the code for building out our environment below. However, the repository at the end of the lesson has the sample code below in a fully functioning webpack environment. If you build out this project yourself, you should include a webpack environment - whether you want to build that yourself or use the repository at the end of the lesson. Note that because we aren't testing, we don't need a __tests__ directory. For now, we also don't need a js directory. All of our JS code in this lesson will be in main.js - the same naming convention we've been using with webpack projects. So for the code example below, we really just need to look at two files: index.html and main.js.

Let's start with the HTML, which is very simple:

index.html
<html lang="en-US">
<head>
  <title>Weather</title>
</head>
<body>
  <div class="container">
    <h1>Get Weather Conditions From Anywhere!</h1>
    <label for="location">Enter a location:</label>
    <input id="location" type="text">
    <button class="btn-success" id="weatherLocation">Get Current Temperature and Humidity</button>
    <div class="showErrors"></div>
    <div class="showHumidity"></div>
    <div class="showTemp"></div>
  </div>
</body>
</html>

We have a simple form input for a location. We also have several divs for showing errors, the temperature, and the humidity.

Now let's look at the code for the API call:

main.js
import $ from 'jquery';
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import './css/styles.css';

$(document).ready(function() {
  $('#weatherLocation').click(function() {
    const city = $('#location').val();
    $('#location').val("");

    let request = new XMLHttpRequest();
    const url = `http://api.openweathermap.org/data/2.5/weather?q=${city}&appid=[YOUR-API-KEY-HERE]`;

    request.onreadystatechange = function() {
      if (this.readyState === 4 && this.status === 200) {
        const response = JSON.parse(this.responseText);
        getElements(response);
      }
    };

    request.open("GET", url, true);
    request.send();

   function getElements(response) {
      $('.showHumidity').text(`The humidity in ${city} is ${response.main.humidity}%`);
      $('.showTemp').text(`The temperature in Kelvins is ${response.main.temp} degrees.`);
    }
  });
});

We start with our import statements. We have a click handler that grabs a city value from a form, stores that value in a variable called city, and then clears the form field with $('#location').val("");. This part is all review.

The new code starts with the following line:

let request = new XMLHttpRequest();

We instantiate a new XMLHttpRequest (XHR for short) object and store it in a variable called request. The name XMLHttpRequest is a bit misleading. These objects are used to interact with servers - exactly what we want to do with API calls. They are not specific to XML requests. As we mentioned before, XML is one relatively common data format that APIs use. However, JSON is much more common these days - and XMLHttpRequest objects can be used with JSON and other types of data as well, not just XML.

Next, we save the URL for our API call in a variable:

const url = `http://api.openweathermap.org/data/2.5/weather?q=${city}&appid=[YOUR-API-KEY-HERE]`;

This isn't necessary but it makes our code a bit easier to read. Note that you'll need to put your own API key in [YOUR-API-KEY-HERE] for the code to work correctly. (We'll learn how to protect our API key in the next lesson.) Our string is a template literal with an embedded expression (${city}) so the value the user inputs into the form is passed directly into our URL string via our city variable.

The rest of the code consists of three parts:

  • A function that listens for any changes to the readyState of the XMLHttpRequest
  • The actual processing and sending of the request
  • A function that will be used as a callback to display results in the browser

First, let's look at the function that listens for changes to the XMLHttpRequest:

request.onreadystatechange = function() {
  if (this.readyState === 4 && this.status === 200) {
    const response = JSON.parse(this.responseText);
    getElements(response);
  }
};

Our XMLHttpRequest object has a property called onreadystatechange. We can set this property to the value of a function that does whatever we want. In the example above, we have an anonymous function (an unnamed function) which is set to the value of that property.

We could even update the code to just log when the ready state changes:

request.onreadystatechange = function() {
  console.log(this.readyState);
};

If we did, we'd see the following in the console. The comments have been added (you won't see the comments in the console if you try this yourself):

1 // Opened
2 // Headers Received
3 // Loading
4 // Done

These numbers correspond to the different states our XMLHttpRequest object can be in. (You wouldn't see 0, which corresponds to Unsent because this is the initial state - and the readyState hasn't changed yet.)

Note: If you try this in the console yourself, ESLint will freak out with a no-unused-vars error. This is because the getElements() function we define later in the code is no longer used. You'll need to temporarily comment it out to soothe ESLint. Also, make sure to return the code to its original state after you're done.

We don't want to do anything until the readyState is 4 because the data transfer won't be complete yet. This is classic async at work. Once this.readyState === 4 and this.status === 200, we'll do something with the data. Why does this.status === 200 need to be part of our conditional? In the last lesson, we mentioned that a 200 response indicates a successful API call. In other words, this conditional states that the API call must be successful and the data transfer must be complete before our code processes that data.

Once the conditional is met, we run the following line of code:

const response = JSON.parse(this.responseText);

As you may have guessed, this.responseText is another built-in property of XMLHttpRequest objects. It's automatically populated once a response is received from a server. By now, it should be clear that XMLHttpRequest objects are really powerful and do a lot of work for us.

We parse this.responseText with JavaScript's built-in JSON.parse method. This ensures that the data is properly formatted as JSON data. Otherwise, our code won't recognize the data as JSON and we'll get an error when we try to use dot notation to get data from it. The JSON.parse() method is essential for working with APIs. As we mentioned in a previous lesson, other programming languages also have methods for parsing JSON as well.

Next, we'll make a callback with the data stored in the response variable:

getElements(response);

When a function calls another function, it's called a callback. We'll cover this more in just a moment.

Before we do, let's go into a bit more detail about XMLHttpRequest objects. We can see exactly what properties an XMLHttpRequest object has by adding a breakpoint inside our conditional and then running the code in the browser. (You don't need to do this right now - it's fine to just look at the image below - but we recommend taking a closer look at an XMLHttpRequest object at some point.)

request.onreadystatechange = function() {
  if (this.readyState === 4 && this.status === 200) {
    debugger;
    ...
  }
};

Adding a breakpoint from the Sources tab is better - the snippet above just shows where the breakpoint should go.

If we take a look at the XMLHttpRequest object in the console, it will look something like this:

This image shows the properties of an XMLHttpRequest object. It has a lot of properties.

As you can see, an XMLHttpRequest object has a lot of functionality. You don't need to worry about most of these properties right now. However, there are a few that will be helpful during this section:

responseText: We've already discussed this one. It includes the text of the response. (There's also a response property which has the same text here.)

status: The status is the API status code. A 200 means it was successful. There are many other codes such as 404 not found and so on. We will use the status in a future lesson.

statusText: We'll see here that it's "OK". That's standard with a 200 status code. It means we are good to go! However, if something went wrong, this is where we might get a more detailed error message such as "not found" or "not authorized."

Now let's return to our new code:

let request = new XMLHttpRequest();
const url = `http://api.openweathermap.org/data/2.5/weather?q=${city}&appid=[YOUR-API-KEY-HERE]`;

request.onreadystatechange = function() {
  if (this.readyState === 4 && this.status === 200) {
    const response = JSON.parse(this.responseText);
    getElements(response);
  }
};

// We've covered everything except for the two lines below!
request.open("GET", url, true);
request.send();

We've discussed everything but the last two lines (the comment highlights what we haven't covered yet).

At this point in our code, we've created a new XMLHttpRequest object and added a function to the onreadystatechange property to listen for changes to the object's ready state, but we haven't actually done anything with the object yet. We still need to open and send the request.

 request.open("GET", url, true);
 request.send();

XMLHttpRequest.open() takes three arguments: the method of the request (in this case GET), the url (which we stored in a variable called url), and a boolean for whether the request should be async or not. Once again, we want the request to be async; we don't want the browser to freeze up for our users! For the API calls we make in this section, the three arguments will almost always be the same - the only exception will be if you make a "POST" or other type of request instead of "GET".

Once we've opened the request, we send it. As we've already discussed, the readyState of the XMLHttpRequest object will change while the function we've attached to the object's onreadystatechange will trigger each time the readyState changes. Finally, when our conditional is triggered in the function we've attached to the onreadystatechange property, our getElements() function will be called.

When a function calls another function, it is known as a callback. Callbacks can get confusing very quickly, especially when one function calls another which then calls another and so on. For that reason, they can be very intimidating for beginners. When you see examples of scary-looking callbacks out in the real world, just remember, a callback is just a function calling another function. We will talk about why callbacks can be so scary in a future lesson when we discuss a concept known as "callback hell."

For now, it's important to know that callbacks are one way for JavaScript developers to deal with async code. A long time ago, it was the only way to deal with async code. Fortunately, there are new tools at our disposal that will make our lives easier. We'll learn about some of these tools later in this section.

The reason we need to use a callback here is because we need to wait until our conditional is triggered before we call getElements(). Remember that JavaScript is non-blocking. It's going to keep running code even if some of it is async.

Let's take a look at what will happen if we don't use a callback.

// Note: This code will not work! It's meant to show why we need to structure our code to use a callback.

    let response;

    request.onreadystatechange = function() {
      if (this.readyState === 4 && this.status === 200) {
        response = JSON.parse(this.responseText);
      }
    };

    request.open("GET", url, true);
    request.send();
    getElements(response);

In the code above, when we call request.send(), our request is sent to the server. Remember that this takes time. The server is going to accept (or deny) our request and send a response. We have to wait for that response to load and then parse it. But JavaScript is non-blocking. That means it's not going to wait until request.send() is done before it moves on. getElements(response) will be called immediately and we'll get the following error:

Cannot read property 'main' of undefined

This is a classic async issue. request.send() is async but getElements(response) is not. The code will continue to run, so the response will still be undefined when getElements() is called. The response will eventually be defined, but our code will break before that happens.

This is why we need a callback. Let's take a look at our original code again:

    request.onreadystatechange = function() {
      if (this.readyState === 4 && this.status === 200) {
        const response = JSON.parse(this.responseText);
        getElements(response);
      }
    };

...

    function getElements(response) {
      $('.showHumidity').text(`The humidity in ${city} is ${response.main.humidity}%`);
      $('.showTemp').text(`The temperature in Kelvins is ${response.main.temp} degrees.`);
    }

In this code, getElements(response) won't be called until the conditional becomes true. In other words, by using a callback, we ensure the function doesn't run until after we get a response from the server.

Async code is one of many important use cases for callbacks. Callbacks can help us control the order that functions should run. If we need a sync function to run immediately after an async function, then we can use a callback to make sure the code runs in the order we expect.

Of course, things can get weird fast when we need a series of sync and async functions to run in a specific order. That leads to callback hell - but that's a subject for a future lesson.

Before we move on, there's one more thing to note: to access humidity or temp, we need to parse the JSON body of the API call. In the case of humidity, that's response.main.humidity. response just matches the name of the function's parameter - it could be something different such as text or body depending on what we decide to name the parameter. main.humidity is specific to the API call. This will always vary from API to API. That's why it's so important to test API calls and practice parsing JSON - depending on the values you want to grab from the data, the dot notation you use could vary greatly. If you ever get stumped when you're parsing JSON, just review the last lesson again - and don't forget to parse the JSON outside of your code (such as in the console) to get it working first. That means making one API call, copying the body to the console or a text editor, and then parsing until you get the value you want. Don't just keep making API calls and altering your code directly - you'll waste API calls and potentially run into problems debugging as well.

In this lesson, we learned how to create and send a XMLHttpRequest object. Doing so should give you a better understanding of how JavaScript makes HTTP requests. Just as importantly, we discussed how we can use callbacks to make sure our code runs in the order we expect.

Later in this section, we'll learn more concise ways to make API calls. While you may find those approaches preferable to building an XMLHttpRequest object, it's still really important to understand the basics of how an XMLHttpRequest object works.

Lesson 7 of 29
Last updated April 8, 2021