Fetch Request
Happy Mother's Day! 💐
It's been another busy couple of weeks. I went down to visit the family and play Bill Baird's release show in San Antonio last weekend. This week we went to a farm dinner out in Paige and then last night I performed at one of those musical yoga events. A lot of community and a lot of art. So many blessings.
I'm picking up (sort of) where I left off in the last blog post. XHR has been wonderful for the transfer of data around the web, but a few years ago the Fetch API was introduced, which has largely replaced XHR usage.
In this blog post, I'm going to rebuild the app that was built in the last post. It has the same two buttons. If you click one, it fetches some data, and if you click the other it sends some data. I'll construct a basic fetch
request, sending and receiving JSON, and handling errors. Once more, all credit goes to Academind for this YouTube video that taught me all of this.
Initial app setup
Our index.html
file is almost exactly the same as last time. I've changed the text in the <title></title>
tags from "XHR Request" to "Fetch Request", and instead of loading xhr.js
, I'm loading fetch.js
. Nothing else is going to change in this file in this blog post.
<!-- index.html --><!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=XHR Request, initial-scale=1.0" /> <title>Fetch Request</title> <script src="./fetch.js" defer></script> </head> <body> <section id="control-center"> <button id="get-btn">GET data</button> <button id="post-btn">POST data</button> </section> </body></html>
The start of fetch.js
is also going to be the same as the start of xhr.js
:
// fetch.jsconst getBtn = document.getElementById("get-btn");const postBtn = document.getElementById("post-btn");
const getData = () => {};
const postData = () => {};
getBtn.addEventListener("click", getData);postBtn.addEventListener("click", postData);
The API we're using
I'm still using the API over at reqres.in, which allows us to fetch and send dummy data via Fetch requests and get real responses back. You can use any test API endpoint service that you would like. This one is easy in that you don't need any authentication and you're not receiving or sending any real data.
Fetch a list of users (GET method)
So let's start adjusting the getData
function to request a list of users from the API.
// ...in fetch.jsconst getData = () => { // 1. Hit the endpoint to retrieve data response, this returns a Promise fetch("https://reqres.in/api/users") .then((response) => { // 2. Specify we want JSON, not a ReadableStream; this returns another Promise return response.json(); }) .then((responseData) => { console.log(responseData); });};
In XHR, we had to construct an xhr
object, and then construct a Promise. Fetch returns a Promise out of the box, so we simply call fetch()
and pass it the url endpoint we want to fetch. We can then process the data using .then()
methods. By default, Fetch's response is a ReadableStream, but in this case we want JSON. We call the .json()
method on the response to convert the response to JSON. We add another .then()
method, and console.log()
out our data.
This is all we need so far to make a simple request. If we reload index.html
in the browser, navigate to Dev Tools > Network, and then press the GET data
button, we can see that the browser made the request for us.
On the Headers tab in Dev Tools > Network, we can see the headers for the request:
And on the Preview tab in Dev Tools > Network, we can preview the data we fetched:
Here is our full fetch.js
file at the moment:
// fetch.jsconst getBtn = document.getElementById("get-btn");const postBtn = document.getElementById("post-btn");
const getData = () => { // 1. Hit the endpoint to retrieve data response, this returns a Promise fetch("https://reqres.in/api/users") .then((response) => { // 2. Specify we want JSON, not a ReadableStream; this returns another Promise return response.json(); }) .then((responseData) => { console.log(responseData); });};
const postData = () => {};
getBtn.addEventListener("click", getData);postBtn.addEventListener("click", postData);
Slight refactor
So at this point I'll refactor the code we just wrote so that we can reuse everything we possibly can for both GET and POST requests.
I will define a general use sendFetchRequest
function that accepts as its parameters method
and url
. This function will hit the url
we specify with the method
. I'll keep the JSON parsing step in this function as well, but pass that JSON onto the function that calls it through the Promise chain. In this case it's just getData()
. getData()
in turn will handle use that JSON data in whatever manner I see fit. Here is what that looks like:
// fetch.jsconst getBtn = document.getElementById("get-btn");const postBtn = document.getElementById("post-btn");
const sendFetchRequest = (method, url) => { return fetch(url, { method, }).then((response) => { return response.json(); });};
const getData = () => { sendFetchRequest("GET", "https://reqres.in/api/users").then( (responseData) => { console.log(responseData); } );};
const postData = () => {};
getBtn.addEventListener("click", getData);postBtn.addEventListener("click", postData);
I'm now calling sendFetchRequest()
inside of getData()
and passing it the GET method as well as the url
endpoint. When it fulfills, I console.log()
that response data. This has set me up nicely to build out my postData()
function to send data.
Registering a user (POST method)
So in order to send data, I can use the sendFetchRequest()
function I created in the last section, pass it the POST method, the endpoint I want to send the request to, and an object with the data that I want to send. It might look something like this:
// ...in fetch.jsconst postData = () => { sendFetchRequest("POST", "https://reqres.in/api/register", { email: "test@test.com", password: "tester", }).then((responseData) => { console.log(responseData); });};
I need to make a few more modifications to sendFetchRequest()
for this to work. My helper function needs to accept the data
object as its third parameter and convert that data
object to a JSON string. Here's how I do that:
// ...in fetch.jsconst sendFetchRequest = (method, url, data) => { return fetch(url, { method, body: JSON.stringify(data), headers: data ? { "Content-Type": "application/json", } : {}, }).then((response) => { return response.json(); });};
I added a third parameter, data
, which will be undefined
for the getData()
call, but will have a value for postData
. I JSON.stringify()
the data
, and pass it to fetch
as a body
property in the options
object. I also need to tell fetch
to expect to receive JSON data, and I do this by setting a header of 'Content-Type': 'application/json'
. Of course, I only need to do this if I have data
. So I use the following ternary operator:
headers: data ? { "Content-Type": "application/json", } : {};
At this point, if I press the POST data
button in the browser to send the request and check the console, I'll see that I get an error:
POST https://reqres.in/api/register 400 (Bad Request){error: 'Note: Only defined users succeed registration'}
Like with XHR, this is to be expected as I am not sending the correct values for the email
and password
props that the API endpoint wants. If I send it with their desired values...
// ...in fetch.jsconst postData = () => { sendFetchRequest("POST", "https://reqres.in/api/register", { email: "eve.holt@reqres.in", password: "pistol", }).then((responseData) => { console.log(responseData); });};
...then I get a success:
{id: 4, token: 'QpwL5tke4Pnpja7X4'}
Excellent. So the full fetch.js
file contents at the moment, for the success:
// fetch.jsconst getBtn = document.getElementById("get-btn");const postBtn = document.getElementById("post-btn");
const sendFetchRequest = (method, url, data) => { return fetch(url, { method, body: JSON.stringify(data), headers: data ? { "Content-Type": "application/json", } : {}, }).then((response) => { return response.json(); });};
const getData = () => { sendFetchRequest("GET", "https://reqres.in/api/users").then( (responseData) => { console.log(responseData); } );};
const postData = () => { sendFetchRequest("POST", "https://reqres.in/api/register", { email: "eve.holt@reqres.in", password: "pistol", }).then((responseData) => { console.log(responseData); });};
getBtn.addEventListener("click", getData);postBtn.addEventListener("click", postData);
And for the error:
// fetch.jsconst getBtn = document.getElementById("get-btn");const postBtn = document.getElementById("post-btn");
const sendFetchRequest = (method, url, data) => { return fetch(url, { method, body: JSON.stringify(data), headers: data ? { "Content-Type": "application/json", } : {}, }).then((response) => { return response.json(); });};
const getData = () => { sendFetchRequest("GET", "https://reqres.in/api/users").then( (responseData) => { console.log(responseData); } );};
const postData = () => { sendFetchRequest("POST", "https://reqres.in/api/register", { email: "test@test.com", password: "tester", }).then((responseData) => { console.log(responseData); });};
getBtn.addEventListener("click", getData);postBtn.addEventListener("click", postData);
Handling errors
So we've seen a case where we get error messages back. How do we handle them in Fetch? I'll start by adding a .catch()
method to the Promise chain within postData()
:
// ...in fetch.jsconst postData = () => { sendFetchRequest("POST", "https://reqres.in/api/register", { email: "test@test.com", password: "tester", }) .then((responseData) => { console.log(responseData); }) .catch((err) => { console.error(err); });};
If I press the POST data button, I do indeed get an error:
{error: 'Note: Only defined users succeed registration'}
But this isn't quite right. I need to update my helper function to actually throw the error, along with the details I get back from the API call. As with XHR, I can do so by checking the status code that fetch()
returns, then throwing an error and rejecting the Promise:
// ...in fetch.jsconst sendFetchRequest = (method, url, data) => { return fetch(url, { method, body: JSON.stringify(data), headers: data ? { "Content-Type": "application/json", } : {}, }).then((response) => { if (response.status >= 400) { return response.json().then((errorResponseData) => { const error = new Error("Something went wrong!"); error.data = errorResponseData; throw error; }); } return response.json(); });};
So I've thrown a new Error with a custom message, "Something went wrong!"
, and appended the response I got back from the API as a data
property on that Error. Back over in our .catch()
block, I can read both of these like so:
const postData = () => { sendFetchRequest("POST", "https://reqres.in/api/register", { email: "test@test.com", password: "tester", }) .then((responseData) => { console.log(responseData); }) .catch((err) => { console.error(err, err.data); });};
So in this way, I am handling any errors that come back from the API by rejecting them and passing them onto my Promise .catch()
block.
Here is my fully finished fetch.js
file:
// fetch.jsconst getBtn = document.getElementById("get-btn");const postBtn = document.getElementById("post-btn");
const sendFetchRequest = (method, url, data) => { return fetch(url, { method, body: JSON.stringify(data), headers: data ? { "Content-Type": "application/json", } : {}, }).then((response) => { if (response.status >= 400) { return response.json().then((errorResponseData) => { const error = new Error("Something went wrong!"); error.data = errorResponseData; throw error; }); } return response.json(); });};
const getData = () => { sendFetchRequest("GET", "https://reqres.in/api/users").then( (responseData) => { console.log(responseData); } );};
const postData = () => { sendFetchRequest("POST", "https://reqres.in/api/register", { // error case: email: "test@test.com", password: "tester",
// success case: // email: 'eve.holt@reqres.in', // password: 'pistol', }) .then((responseData) => { console.log(responseData); }) .catch((err) => { console.error(err, err.data); });};
getBtn.addEventListener("click", getData);postBtn.addEventListener("click", postData);
Summary
Fetch is largely seen as a modern improvement on the older XHR request pattern. Where in XHR you had to explicitly create your own Promise, create an xhr
object, and specify values for xhr
methods like .open()
, .responseType
, .setRequestHeader()
, .onload
, .onerror
, and .send()
, Fetch compartmentalizes all of that into a single fetch()
function call, which by default returns a Promise.
However, it's still not a totally seamless API. Error handling is often seen as complicated and repetitive. Similar to XHR, Fetch will only throw an error itself if there is a network failure, you pass a bad URL, or something else goes wrong at the system level. It doesn't consider response status codes greater than 400 to be errors, so you have to manually check for those yourself, then throw the error, and then specify your .catch()
block to handle them.
Our current archecture reuses a lot of code to make the requests, but handling errors is still attached to each instance of a function call. I dislike that because it still feels like a lot of boilerplate. If we had a dozen buttons that called different endpoints, I wouldn't want to have to repeat the .catch()
logic for each one. Oh well. This is still a pretty cool API, and at this point has very good support across the browser and JavaScript ecosystem.