ajax
HTTP
Fetch API
Think of a fetch as a simple JavaScript Promise. You send a request to the server/API and expect to receive a response. The Fetch API gives us the fetch()
method, which allows us to access those requests and responses using JavaScript.
Let's look at what a simple fetch looks like:
1fetch('examples/example.json') 2 .then(response => { 3 // Here is where you put what you want to do with the response 4 }) 5 .catch(error => { 6 console.log('Oh No! There was a problem: \n', error); 7 });
What is happening here?
.then
method for use..catch
is passed with the appropriate error as a parameter.
Note that an error like 404 (a bad response) is still a valid response (the server returned something to us) and therefore wouldn't be considered an uncompleted response. So, in the example above, it would not automatically fall back to the .catch
.How do we check for a successful response?
Since the fetch promise would only reject a request if it was unable to complete it, we need to manually validate if a response is good or throw an error if it's not.
A generic response from a server looks something like this when logged on the console:
1[object Response] { 2 body: (...) 3 bodyUsed: false 4 headers: Headers {} 5 ok: true 6 redirected: false 7 status: 200 8 statusText: "OK" 9 type: "cors" 10 url: "https://assets.breatheco.de/apis/fake/todos/user/Gmihov" 11}
With that in mind, to evaluate the status of a response, you can use its properties:
Now we can update the example from above to validate the response
1fetch('examples/example.json') 2 .then(response => { 3 if (!response.ok) { 4 throw new Error(response.statusText); 5 } 6 // Here is where you put what you want to do with the response 7 }) 8 .catch(error => { 9 console.log('Looks like there was a problem: \n', error); 10 });
Now what's happening?
.then
to be used as you specify..catch
.Why do we need this?
To prevent bad responses from going down the chain and breaking your code later on.
We need to throw this error manually because, as explained above, error messages received within a response from the server do not register automatically as an error and do not show up in the .catch
method.
The result will be that the fetch will deliver nothing, and yet the client will be clueless that something has gone wrong.
Now What?
Now we need to "read" the response in order to access the body of the response.
As you already know, the only data that can travel over an HTTP connection is in plain text format. Therefore, we need to convert the plain text from the body of the response into meaningful JavaScript format.
Luckily, there's a method for that: .json()
; which we apply to the response as response.json()
.
1fetch('examples/example.json') 2 .then(response => { 3 if (!response.ok) { 4 throw Error(response.statusText); 5 } 6 // Read the response as JSON 7 return response.json(); 8 }) 9 .then(responseAsJson => { 10 // Do stuff with the JSONified response 11 console.log(responseAsJson); 12 }) 13 .catch(error => { 14 console.log('Looks like there was a problem: \n', error); 15 });
Now what is going on?
Simple. Think of it in separate steps.
Fetch the resource at the given path. (Fetch gets the path to the resource and returns a promise that will resolve to a response object).
Then validate the response. (This checks to see if the response is valid (200s). If not, skip to step 5).
Read the response as JSON.
Log the result. (The result is the JSON data received from the body of the response).
Catch any errors.
Now that you have seen the basics, we can compose more advanced fetches.
The default request method is the "GET" method; which is what we have seen so far.
Here's an example of a POST method that creates a new user:
1fetch('https://example.com/users.json', { 2 method: 'POST', 3 mode: 'cors', 4 redirect: 'follow', 5 headers: new Headers({ 6 'Content-Type': 'text/plain' 7 }) 8}) 9 .then(res => res.json()) 10 .then(response => { /* handle response */ }) 11 .catch(error => console.error(error));
Note: that this example fetch is posting (sending to the server) data in plain text format. In modern front end development, this is less common. The most common content type for our methods will be the
application/json
format, as seen in the following example:
1fetch('https://example.com/users', { 2 method: 'PUT', // or 'POST' 3 body: JSON.stringify(data), // data can be a 'string' or an {object} which comes from somewhere further above in our application 4 headers: { 5 'Content-Type': 'application/json' 6 } 7}) 8 .then(res => { 9 if (!res.ok) throw Error(res.statusText); 10 return res.json(); 11 }) 12 .then(response => console.log('Success:', response)) 13 .catch(error => console.error(error));
โ Did you notice something new above?
The "body" of the fetch is where we place the data that we want to send to the server for permanent storage with our POST or PUT requests.
Because we can only send plain text over HTTP, we have to convert our data from its original JS format to a string. We do that with the JSON.stringify()
method.
Requests with the GET or DELETE methods do not require a body since normally they are not expected to send any data, however, you can include a body in those requests as well, if needed.
HTTP headers allow us to perform additional actions on the request and response. You can set request headers as you see above.
One of the most common headers needed is the 'Content-Type' header. It signals to the recipient of the request (the server) how it should treat the data contained in the body of the request. Because most of the time we send data in some JavaScript format which is then stringified, we need to instruct the server to convert the string that it receives back into a JSON format, as seen in line 5 above.
Headers can be sent in a request and received in a response.
Therefore, you can use the headers of the response you receive from the server to check the content type of its body and make sure you are receiving the right format before going any further in the process. An example of this would be:
1fetch('https://example.com/users') 2 .then(response => { 3 let contentType = response.headers.get("content-type"); 4 if (contentType && contentType.includes("application/json")) { 5 return response.json(); 6 } 7 throw new TypeError("Sorry, There's no JSON here!"); 8 }) 9 .then(jsonifiedResponse => { /* do whatever you want with the jsonified response */ }) 10 .catch(error => console.log(error));
Note that a Header method will throw a
TypeError
if the name used is not a valid HTTP header name. A list of valid headers can be found here.
JavaScript provides us an alternative way to make HTTP requests using fetch()
with Async/Await
async
makes a function return a Promiseawait
makes a function wait for a PromiseLet's start with the GET method and analyze it
1const getData = async () => { 2 const response = await fetch('https://example.com/users'); 3 if (response.ok) { 4 const data = await response.json(); 5 return data; 6 } else { 7 console.log('error: ', response.status, response.statusText); 8 /* Handle the error returned by the HTTP request */ 9 return {error: {status: response.status, statusText: response.statusText}}; 10 }; 11};
๐ Remember that GET is the default method. Therefore, it is not mandatory to write the second parameter of
fetch()
Let's Analyze This Function
๐ Note: The HTTP protocol will always provide a response. Whether that response is good or not will be known through the HTTP Response Status Codes.
Now that we know how it works, let's see an example of the POST method
1const createData = async () => { 2 const response = await fetch('https://example.com/users', { 3 method: 'POST', 4 body: JSON.stringify(dataToSend), // the variable dataToSend can be a 'string' or an {object} that comes from somewhere else in our application 5 headers: { 6 'Content-Type': 'application/json' 7 } 8 }); 9 if (response.ok) { 10 const data = await response.json(); 11 return data; 12 } else { 13 console.log('error: ', response.status, response.statusText); 14 /* Handle the error returned by the HTTP request */ 15 return {error: {status: response.status, statusText: response.statusText}}; 16 }; 17};
Here we add the second parameter of fetch()
to include the method, body, and headers.
Example of a request with the PUT method
1const updateData = async () => { 2 const response = await fetch('https://example.com/users', { 3 method: 'PUT', 4 body: JSON.stringify(dataToSend), // the variable dataToSend can be a 'string' or an {object} that comes from somewhere else in our application 5 headers: { 6 'Content-Type': 'application/json' 7 } 8 }); 9 if (response.ok) { 10 const data = await response.json(); 11 return data; 12 } else { 13 console.log('error: ', response.status, response.statusText); 14 /* Handle the error returned by the HTTP request */ 15 return {error: {status: response.status, statusText: response.statusText}}; 16 }; 17}; 18 19
Example of a request with the DELETE method.
1const deleteData = async () => { 2 const response = await fetch('https://example.com/users', { 3 method: 'DELETE', 4 }); 5 if (response.ok) { 6 const data = await response.json(); 7 return data; 8 } else { 9 console.log('error: ', response.status, response.statusText); 10 /* Handle the error returned by the HTTP request */ 11 return {error: {status: response.status, statusText: response.statusText}}; 12 }; 13};