Lesson Wednesday

Now that we have experience creating and using XMLHttpRequest objects to make API calls, let's simplify our code even further. While XMLHttpRequest objects get the job done, there are other methods that use XMLHttpRequest objects under the hood while making our lives easier as developers.

In this lesson, we'll focus on the Fetch API, which will allow us to make a basic API call with a single line of code. Note that the Fetch API is called an API because it provides a simple interface we can use in our applications. Remember, that's all an API is: an application programming interface. It is not called the Fetch API because it is used to make API calls.

In addition to using XMLHttpRequest under the hood, the fetch() method also returns a promise. In other words, we can use fetch() instead of manually creating both promises and XMLHttpRequest objects.

Here's a basic GET request to the Open Weather API with fetch():

fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.API_KEY}`);

That's it. Because the fetch() method returns a promise, we can use Promise.prototype.then() to handle the response. This is so much easier, though there will be a bit of a tradeoff when it comes to handling the response.

Let's start by updating our weather-service.js file:

weather-service.js
export default class WeatherService {  
  static getWeather(city) {
    return fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.API_KEY}`)
      .then(function(response) {
        if (!response.ok) {
          throw Error(response.statusText);
        }
        return response.json();
      })
      .catch(function(error) {
        return error;
      })
  }
}

Remember that our function needs to return a promise. Since fetch() itself returns a promise, we can just return that.

That's the easy part.

However, there's a bit of a tradeoff when handling the fetch() response. fetch() almost never rejects a promise. There has to be a serious network error for the promise to be rejected. That's not very helpful for us - we need some kind of error handling if the response is not 200 OK. When we used a promise without fetch(), we were able to reject responses that weren't 200 OK, but now we can't do that.

For that reason, we need to handle the response to fetch() in our service logic. We could handle it in our UI logic but it wouldn't be good separation because the error handling for our API call has nothing to do with the user interface.

Fortunately, the response object that fetch() returns includes an ok property. If the API call has a 200 status (remember, that's "ok"), this property will be set to true. If it doesn't, it will be set to false. So if !response.ok, our code will immediately throw an error.

If an error is thrown, control immediately reverts to the catch block. So if our code throws an error, it won't reach return response.json();. Instead, control will go to the catch block and the error will be returned. If the response is ok, the conditional won't be triggered and our code will return response.json(). So this is our first opportunity to practice using Promise.prototype.catch(). Note that it's generally a good idea to add this method when we are working with promises for the purpose of error handling. Even if we don't manually throw an error in our application, it's possible that our code will throw an error anyway, especially if there's a typo or something is broken. You can always return an Error object from a promise in the exact fashion we use above - as long as you write code to handle it as well. We'll be covering that in a moment.

Now for another wrinkle: fetch() returns a promise which, when resolved, is a stream that our code must read and convert to JSON. That's what's happening when we call response.json(). In other words, the data is streaming and being retrieved now but the data transfer won't be complete until later. More async! Making the API request is the first async operation and reading the data stream from the response is the second async operation.

So our method returns a promise which returns a second promise. Even if the catch block is triggered, our method will still return a promise because the return of Promise.prototype.then() is itself a promise - even if we are just running sync code like our error handling.

Ultimately, our method will return one of two things:

  • The data of a successful response (ideally), or
  • An error message if the API call goes wrong.

Next, we need to update our UI code.

main.js

// Imports and clearFields() omitted for brevity.

function getElements(response) {
  if (response.main) {
    $('.showHumidity').text(`The humidity in ${response.name} is ${response.main.humidity}%`);
    $('.showTemp').text(`The temperature in Kelvins is ${response.main.temp} degrees.`);
  } else {
    $('.showErrors').text(`There was an error: ${response.message}`);
  }
}

$(document).ready(function() {
  $('#weatherLocation').click(function() {
    let city = $('#location').val();
    clearFields();
    WeatherService.getWeather(city)
      .then(function(response) {
        getElements(response);
      });
  });
});

First, this is a good opportunity to separate out our code further, taking code that alters the DOM and putting it in its own getElements() function. So our code is a bit cleaner once we do that.

Here's the updated code related to the API call itself:

main.js
WeatherService.getWeather(city)
  .then(function(response) {
    getElements(response);
  });

This code is very simple now - let our static method return a promise, wait for the promise to fulfill with Promise.prototype.then() and then make a callback. Note that we don't need to save the return of our static method in a variable. We can just chain Promise.prototype.then() directly to the method because the method returns a promise.

Also, note that we put .then() on a new line. We did the same in our static method as well. This spacing is common for readability, especially when chaining multiple promises together. For instance, we might see something like this out in the wild:

// Pseudo-code!

promise
  .then(doSomething)
  .then(doSomethingElse)
  .then(doOneLastThing)
  .catch(takeCareOfThatError)

As we can see, putting each .then() on a newline makes our code easier to read. Finally, the catch block handles errors that occur anywhere inside this chain of promises. We will cover this process further in a future optional lesson on chaining promises.

Now let's take a look at our callback:

main.js
function getElements(response) {
  if (response.main) {
    $('.showHumidity').text(`The humidity in ${response.name} is ${response.main.humidity}%`);
    $('.showTemp').text(`The temperature in Kelvins is ${response.main.temp} degrees.`);
  } else {
    $('.showErrors').text(`There was an error: ${response.message}`);
  }
}

Our callback has a conditional designed to display different content in the DOM depending on whether there was an error. If there isn't an error, the response will have a main property, which means if (response.main) will be truthy. Remember, this main property is unique to responses from the OpenWeather API - we'd have to format our conditional differently for the response body of other APIs. In this case, if there's a main property we can go ahead and display the humidity and temperature.

However, if the previous promise returns an error, that error will be passed into the response of getElements(). JavaScript Error objects have a handy message property so we can get the message via response.message.

It's important to emphasize that we are grabbing properties from two completely different things in the callback above. If there's a successful API call, we are parsing the body of the API response. If there isn't, we are just grabbing the message property from a JavaScript Error object. There's potential for confusion because the parameter is named response - but that's just the name we gave it.

While the fetch() method is very useful, some developers prefer using XMLHttpRequest objects. You may choose to use either on this week's independent project. Ultimately, even if you prefer using fetch(), it's still important to have a good understanding of XMLHttpRequest objects and promises because fetch() relies on both.

Here's a quick guide to consider which one you might want to use in different situations:

  • Use XMLHttpRequest objects and promises if you want full control over being able to reject a promise.
  • Use fetch() if you don't want to worry about dealing with XMLHttpRequest objects and want any advantages that come with streaming the data instead of waiting for the full response.

Just make sure you get plenty of opportunity to practice both! The more practice you get with different ways of dealing with asynchrony, the stronger you'll be as a coder.

JQuery also provides some useful methods for making API calls as well. We will not cover them in this section, but if you'd like to explore them further, check out the jQuery.get() and jQuery.ajax() methods. There aren't any advantages of using jQuery over fetch() or XMLHttpRequest objects - in fact, we'd generally recommend against using jQuery for this kind of thing, mainly because other libraries such as React are better tools than jQuery for working with the DOM. However, jQuery is still very common out in the wild - so it's good to know about these methods, too.


Example GitHub Repo for API Project with fetch()

Lesson 23 of 25
Last updated more than 3 months ago.