Lab: Fetch API

Concepts: Working with the Fetch API

Overview

This lab walks you through using the Fetch API, a simple interface for fetching resources, as an improvement over the XMLHttpRequest API.

What you will learn

  • How to use the Fetch API to request resources
  • How to make GET, HEAD, and POST requests with fetch

What you should know

What you will need

  • Computer with terminal/shell access
  • Connection to the internet
  • A browser that supports Fetch
  • A text editor
  • Node and npm

1. Get set up

If you have not downloaded the repository, installed Node, and started a local server, follow the instructions in Setting up the labs.

Open your browser and navigate to localhost:8080/fetch-api-lab/app.

If you have a text editor that lets you open a project, open the fetch-api-lab/app folder. This will make it easier to stay organized. Otherwise, open the folder in your computer's file system. The app folder is where you will be building the lab.

This folder contains:

  • echo-servers contains files that are used for running echo servers
  • examples contains sample resources that we use in experimenting with fetch
  • index.html is the main HTML page for our sample site/application
  • js/main.js is the main JavaScript for the app, and where you will write all your code
  • test/test.html is a file for testing your progress
  • package.json is a configuration file for node dependencies

2. Fetching a resource

2.1 Fetch a JSON file

Open js/main.js in your text editor.

Replace the TODO 2.1a comment with the following code:

main.js

if (!('fetch' in window)) {
  console.log('Fetch API not found, try including the polyfill');
  return;
}

In the fetchJSON function, replace TODO 2.1b with the following code:

main.js

fetch('examples/animals.json')
.then(logResult)
.catch(logError);

Save the script and refresh the page. Click Fetch JSON. The console should log the fetch response.

Optional: Open the site on an unsupported browser and verify that the support check conditional works.

Explanation

The code starts by checking for fetch support. If the browser doesn't support fetch, the script logs a message and fails immediately.

We pass the path for the resource we want to retrieve as a parameter to fetch, in this case examples/animals.json. A promise that resolves to a Response object is returned. If the promise resolves, the response is passed to the logResult function. If the promise rejects, the catch takes over and the error is passed to the logError function.

Response objects represent the response to a request. They contain the response body and also useful properties and methods.

2.2 Examine response properties

Find the values of the status, url, and ok properties of the response for the fetch we just made. What are these values? Hint: Look in the console.

In the fetchJSON function we just wrote in section 2.1, replace the examples/animals.json resource with examples/non-existent.json. So the fetchJSON function should now look like:

main.js

function fetchJSON() {
  fetch('examples/non-existent.json')
  .then(logResult)
  .catch(logError);
}

Save the script and refresh the page. Click Fetch JSON again to try and fetch this new resource.

Now find the status, URL, and ok properties of the response for this new fetch we just made. What are these values?

The values should be different for the two files (do you understand why?). If you got any console errors, do the values match up with the context of the error?

Explanation

Why didn't a failed response activate the catch block? This is an important note for fetch and promises—bad responses (like 404s) still resolve! A fetch promise only rejects if the request was unable to complete, so you must always check the validity of the response.

For more information

2.3 Check response validity

We need to update our code to check the validity of responses.

Complete the function called validateResponse in TODO 2.3. The function should accept a response object as input. If the response object's ok property is false, the function should throw an error containing response.statusText. If the response object's ok property is true, the function should simply return the response object.

You can confirm that you have written the function correctly by navigating to app/test/test.html. This page runs tests on some of the functions you write. If there are errors with your implementation of a function (or you haven't implemented them yet), the test displays in red. Passed tests display in blue. Refresh the test.html page to retest your functions.

Once you have successfully written the function, replace fetchJSON with the following code:

main.js

function fetchJSON() {
  fetch('examples/non-existent.json')
  .then(validateResponse)
  .then(logResult)
  .catch(logError);
}

This is promise chaining.

Save the script and refresh the page. Click Fetch JSON. Now the response for examples/non-existent.json should trigger the catch block, unlike in section 2.2. Check the console to confirm this.

Now replace examples/non-existent.json resource in the fetchJSON function with the original examples/animals.json from section 2.1. The function should now look like:

main.js

function fetchJSON() {
  fetch('examples/animals.json')
  .then(validateResponse)
  .then(logResult)
  .catch(logError);
}

Save the script and refresh the page. Click Fetch JSON. You should see that the response is being logged successfully like in section 2.1.

Explanation

Now that we have added the validateResponse check, bad responses (like 404s) throw an error and the catch takes over. This prevents bad responses from propagating down the fetch chain.

2.4 Read the response

Responses must be read in order to access the body of the response. Response objects have methods for doing this.

To complete TODO 2.4, replace the readResponseAsJSON function with the following code:

main.js

function readResponseAsJSON(response) {
  return response.json();
}

(You can check that you have done this correctly by navigating to app/test/test.html.)

Then replace the fetchJSON function with the following code:

main.js

function fetchJSON() {
  fetch('examples/animals.json') // 1
  .then(validateResponse) // 2
  .then(readResponseAsJSON) // 3
  .then(logResult) // 4
  .catch(logError);
}

Save the script and refresh the page. Click Fetch JSON. Check the console to see that the JSON from examples/animals.json is being logged.

Explanation

Let's review what is happening.

Step 1. Fetch is called on a resource, examples/animals.json. Fetch returns a promise that resolves to a Response object. When the promise resolves, the response object is passed to validateResponse.

Step 2. validateResponse checks if the response is valid (is it a 200?). If it isn't, an error is thrown, skipping the rest of the then blocks and triggering the catch block. This is particularly important. Without this check bad responses are passed down the chain and could break later code that may rely on receiving a valid response. If the response is valid, it is passed to readResponseAsJSON.

Step 3. readResponseAsJSON reads the body of the response using the Response.json() method. This method returns a promise that resolves to JSON. Once this promise resolves, the JSON data is passed to logResult. (Can you think of what would happen if the promise from response.json() rejects?)

Step 4. Finally, the JSON data from the original request to examples/animals.json is logged by logResult.

For more information

Solution code

To get a copy of the working code, navigate to the 02-fetching-a-resource folder.

3. Fetch an image

Fetch is not limited to JSON. In this example we will fetch an image and append it to the page.

To complete TODO 3a, replace the showImage function with the following code:

main.js

function showImage(responseAsBlob) {
  var container = document.getElementById('container');
  var imgElem = document.createElement('img');
  container.appendChild(imgElem);
  var imgUrl = URL.createObjectURL(responseAsBlob);
  imgElem.src = imgUrl;
}

To complete TODO 3b, finish writing the readResponseAsBlob function. The function should accept a response object as input. The function should return a promise that resolves to a Blob.

(You can check that you have done this correctly by navigating to app/test/test.html.)

To complete TODO 3c, replace the fetchImage function with the following code:

main.js

function fetchImage() {
  fetch('examples/kitten.jpg')
  .then(validateResponse)
  .then(readResponseAsBlob)
  .then(showImage)
  .catch(logError);
}

Save the script and refresh the page. Click Fetch image. You should see an adorable kitten on the page.

Explanation

In this example an image is being fetched, examples/kitten.jpg. Just like in the previous exercise, the response is validated with validateResponse. The response is then read as a Blob (instead of JSON as in section 2). An image element is created and appended to the page, and the image's src attribute is set to a data URL representing the Blob.

For more information

Solution code

To get a copy of the working code, navigate to the 03-fetching-images folder.

4. Fetch text

In this example we will fetch text and add it to the page.

To complete TODO 4a, replace the showText function with the following code:

main.js

function showText(responseAsText) {
  var message = document.getElementById('message');
  message.textContent = responseAsText;
}

To complete TODO 4b, finish writing the readResponseAsText function.. This function should accept a response object as input. The function should return a promise that resolves to text.

(You can check that you have done this correctly by navigating to app/test/test.html.)

To complete TODO 4c, replace the fetchText function with the following code:

function fetchText() {
  fetch('examples/words.txt')
  .then(validateResponse)
  .then(readResponseAsText)
  .then(showText)
  .catch(logError);
}

Save the script and refresh the page. Click Fetch text. You should see a message on the page.

Explanation

In this example a text file is being fetched, examples/words.txt. Like the previous two exercises, the response is validated with validateResponse. Then the response is read as text, and appended to the page.

For more information

Solution code

To get a copy of the working code, navigate to the 04-fetching-text folder.

5. Using HEAD requests

By default, fetch uses the GET method, which retrieves a specific resource. But fetch can also use other HTTP methods.

5.1 Make a HEAD request

To complete TODO 5.1, replace the headRequest function with the following code:

main.js

function headRequest() {
  fetch('examples/words.txt', {
    method: 'HEAD'
  })
  .then(validateResponse)
  .then(readResponseAsText)
  .then(logResult)
  .catch(logError);
}

Save the script and refresh the page. Click HEAD request. What do you notice about the console log? Is it showing you the text in examples/words.txt, or is it empty?

Explanation

fetch() can receive a second optional parameter, init. This enables the creation of custom settings for the fetch request, such as the request method, cache mode, credentials, and more.

In this example we set the fetch request method to HEAD using the init parameter. HEAD requests are just like GET requests, except the body of the response is empty. This kind of request can be used when all you want is metadata about a file but don't need to transport all of the file's data.

5.2 Optional: Find the size of a resource

Let's look at the Headers of the fetch response from section 5.1 to determine the size of examples/words.txt.

Complete the function called logSize in TODO 5.2. The function accepts a response object as input. The function should log the content-length of the response. To do this, you need to access the headers property of the response, and use the headers object's get method. After logging the the content-length header, the function should then return the response.

Then replace the headRequest function with the following code:

function headRequest() {
  fetch('examples/words.txt', {
    method: 'HEAD'
  })
  .then(validateResponse)
  .then(logSize)
  .then(readResponseAsText)
  .then(logResult)
  .catch(logError);
}

Save the script and refresh the page. Click HEAD request. The console should log the size (in bytes) of examples/words.txt (it should be 74 bytes).

Explanation

In this example, the HEAD method is used to request the size (in bytes) of a resource (represented in the content-length header) without actually loading the resource itself. In practice this could be used to determine if the full resource should be requested (or even how to request it).

Optional: Find out the size of examples/words.txt using another method and confirm that it matches the value from the response header (you can look up how to do this for your specific operating system—bonus points for using the command line!).

For more information

Solution code

To get a copy of the working code, navigate to the 05-head-requests folder.

6. Using POST requests

Fetch can also send data with POST requests.

6.1 Set up an echo server

For this example you need to run an echo server. From the fetch-api-lab/app directory run the following commands:

npm install
node echo-servers/echo-server-cors.js

You can check that you have successfully started the server by navigating to app/test/test.html and checking the 'echo server #1 running (with CORS)' task. If it is red, then the server is not running.

Explanation

In this step we install and run a simple server at localhost:5000/ that echoes back the requests sent to it.

6.2 Make a POST request

To complete TODO 6.2, replace the postRequest function with the following code:

main.js

function postRequest() {
  // TODO 6.3
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: 'name=david&message=hello'
  })
  .then(validateResponse)
  .then(readResponseAsText)
  .then(logResult)
  .catch(logError);
}

Save the script and refresh the page. Click POST request. Do you see the sent request echoed in the console? Does it contain the name and message?

Explanation

To make a POST request with fetch, we use the init parameter to specify the method (similar to how we set the HEAD method in section 5). This is also where we set the body of the request. The body is the data we want to send.

When data is sent as a POST request to localhost:5000/, the request is echoed back as the response. The response is then validated with validateResponse, read as text, and logged to the console.

In practice, this server would be a 3rd party API.

6.3 Use the FormData interface

You can use the FormData interface to easily grab data from forms.

In the postRequest function, replace TODO 6.3 with the following code:

main.js

var formData = new FormData(document.getElementById('myForm'));

Then replace the value of the body parameter with the formData variable.

Save the script and refresh the page. Fill out the form (the Name and Message fields) on the page, and then click POST request. Do you see the form content logged in the console?

Explanation

The FormData constructor can take in an HTML form, and create a FormData object. This object is populated with the form's keys and values.

For more information

Solution code

To get a copy of the working code, navigate to the 06-post-requests folder.

7. Optional: CORS and custom headers

7.1 Start a new echo server

Stop the previous echo server (by pressing Ctrl+C from the command line) and start a new echo server from the fetch-lab-api/app directory by running the following command:

node echo-servers/echo-server-no-cors.js

You can check that you have successfully started the server by navigating to app/test/test.html and checking the 'echo server #2 running (without CORS)' task. If it is red, then the server is not running.

Explanation

The application we run in this step sets up another simple echo server, this time at localhost:5001/. This server, however, is not configured to accept cross origin requests.

7.2 Fetch from the new server

Now that the new server is running at localhost:5001/, we can send a fetch request to it.

Update the postRequest function to fetch from localhost:5001/ instead of localhost:5000/. Save the script, refresh the page, and then click POST Request.

You should get an error indicating that the cross-origin request is blocked due to the CORS Access-Control-Allow-Origin header being missing.

Update fetch in the postRequest function to use no-cors mode (as the error log suggests). Comment out the validateResponse and readResponseAsText steps in the fetch chain. Save the script and refresh the page. Then click POST Request.

You should get a response object logged in the console.

Explanation

Fetch (and XMLHttpRequest) follow the same-origin policy. This means that browsers restrict cross-origin HTTP requests from within scripts. A cross-origin request occurs when one domain (for example http://foo.com/) requests a resource from a separate domain (for example http://bar.com/).

Since our app's server has a different port number than the two echo servers, requests to either of the echo servers are considered cross-origin. The first echo server, however, running on localhost:5000/, is configured to support CORS. The new echo server, running on localhost:5001/, is not (which is why we get an error).

Using mode: no-cors allows fetching an opaque response. This prevents accessing the response with JavaScript (which is why we comment out validateResponse and readResponseAsText), but the response can still be consumed by other API's or cached by a service worker.

7.3 Modify request headers

Fetch also supports modifying request headers. Stop the localhost:5001 (no CORS) echo server and restart the localhost:5000 (CORS) echo server from section 6 (node echo-servers/echo-server-cors.js).

Update the postRequest function to fetch from localhost:5000/ again. Remove the no-cors mode setting from the init object or update the mode to cors (these are equivalent, as cors is the default mode). Uncomment the validateResponse and readResponseAsText steps in the fetch chain.

Now use the Header interface to create a Headers object inside the postRequest function called customHeaders with the Content-Type header equal to text/plain. Then add a headers property to the init object and set the value to be the customHeaders variable. Save the script and refresh the page. Then click POST Request.

You should see that the echoed request now has a Content-Type of plain/text (as opposed to multipart/form-data as it had previously).

Now add a custom Content-Length header to the customHeaders object and give the request an arbitrary size. Save the script, refresh the page, and click POST Request. Observe that this header is not modified in the echoed request.

Explanation

The Header interface enables the creation and modification of Headers objects. Some headers, like Content-Type can be modified by fetch. Others, like Content-Length, are guarded and can't be modified (for security reasons).

7.4 Set custom request headers

Fetch supports setting custom headers.

Remove the Content-Length header from the customHeaders object in the postRequest function. Add the custom header X-Custom with an arbitrary value (for example 'X-CUSTOM': 'hello world'). Save the script, refresh the page, and then click POST Request.

You should see that the echoed request has the X-Custom that you added.

Now add a Y-Custom header to the Headers object. Save the script, refresh the page, and click POST Request.

You should get an error similar to this in the console:

Fetch API cannot load http://localhost:5000/. Request header field y-custom is not allowed by Access-Control-Allow-Headers in preflight response.

Explanation

Like cross-origin requests, custom headers must be supported by the server from which the resource is requested. In this example, our echo server is configured to accept the X-Custom header but not the Y-Custom header (you can open echo-servers/echo-server-cors.js and look for Access-Control-Allow-Headers to see for yourself). Anytime a custom header is set, the browser performs a preflight check. This means that the browser first sends an OPTIONS request to the server, to determine what HTTP methods and headers are allowed by the server. If the server is configured to accept the method and headers of the original request, then it is sent, otherwise an error is thrown.

For more information

Solution code

To get a copy of the working code, navigate to the solution folder.

Congratulations!

You now know how to use the Fetch API to request resources and post data to servers.

Resources