In a modern web app, client-side JavaScript programs react to user interaction, and send HTTP requests to Web APIs to get up-to-date information to display or invoke actions on the server. Client-side programs also need to handle receive data or reported errors from the server.
Web browsers provide two main means to send asynchronous HTTP requests: the traditional XMLHttpRequest, and the new Fetch API. XMLHttpRequest applies callbacks and events to handle asynchronous message handling, while the Fetch API fetch
is based on promises. In this chapter, we’ll use fetch
to interact with Web APIs.
An introduction to fetch()
usually starts with an example that resembles the following. Line 3 assumes the response body is JSON format, and converts it to JavaScript data. Line 4 receives the data, and prints it in the console. The default HTTP method fo fetch()
is GET
.
const endpoint = 'https://jsonplaceholder.typicode.com/users/1';
fetch(endpoint)
.then((response) => response.json())
.then((json) => console.log(json));
You can rewrite the above example using await
, as below. This work inside an async
function, or on top level of a program.
const endpoint = 'https://jsonplaceholder.typicode.com/users/1';
let response = await fetch(endpoint);
let json = await response.json();
console.log(json);
A GET
request cannot have payload in the body. So you can only attach payload in the query string of the URL. For example, some REST APIs enhance the endpoints of collection resources to accept search criteria. The following endpoint retrieves the pending todo of the user with userId=1
.
https://jsonplaceholder.typicode.com/todos?userId=1&completed=false
You can construct the query string in the end of the URL yourself, or you can use the browser built-in object URLSearchParams
to do the encoding.
const endpoint = 'https://jsonplaceholder.typicode.com/todos?'
+ new URLSearchParams({
userId: 1,
completed: false,
});
fetch(endpoint)
.then((response) => response.json())
.then((json) => console.log(json));
The first example of fetch()
above involves two promises.
The browser built-in function fetch()
initiates an HTTP request and returns a promise. Because it will take some time until the browser receives the response, fetch()
is asynchronous. The promise will resolve to a response
object when the browser has received the status line and headers of the response, but before it receives the full response body. The promise rejects if there are network errors. You can check response.ok
to find whether the HTTP status code is in class 200, e.g. 200 Ok
or 204 No content
.
const endpoint = 'https://jsonplaceholder.typicode.com/users/1';
fetch(endpoint).then((response) => {
console.log('ok: ', response.ok, 'status: ', response.status);
console.log('headers: ', [...response.headers]);
// but the response body is not yet ready yet
})
Assuming that the response is ok, you can call the method response.json()
to read the response body, treat it as JSON string, and convert it into a JavaScript object. (Attention!) This method also returns a promise. (Some response body may be large and will take some time to transfer from the server.) The promise resolves to a JavaScript object when the JSON data is ready.
const endpoint = 'https://jsonplaceholder.typicode.com/users/1';
fetch(endpoint).then( (response) => {
let prom = response.json();
prom.then( (json) => {
// 'json' is a JavaScript object converted from the JSON payload
console.log(json);
});
});
The ‘then handler’ only creates a promise and handles it. We can take advantage of chaining ‘then handlers’ of a promise to simplify the code. If the ‘then handler’ returns a new promise, you can chain the ‘then handler’ of the new promise.
const endpoint = 'https://jsonplaceholder.typicode.com/users/1';
fetch(endpoint)
// this handler returns a promise that resolves to json data
.then((response) => response.json())
// this handler receives the json data
.then((json) => console.log(json));
fetch()
does not reject if the server returns an HTTP status code reporting errors (e.g.404
,500
). For example,fetch('https://httpstat.us/500').then( (response) => { console.log('ok: ', response.ok, 'status: ', response.status); console.log('headers: ', [...response.headers]); console.log('url: ', response.url); }).catch( (error) => { console.log('promise rejected: ', error); });
fetch()
rejects when there is network error, or somehow it cannot finish the HTTP request-response transaction. For example,fetch('https://notexist.ipm.edu.mo').then( (response) => { console.log('ok: ', response.ok, 'status: ', response.status); }).catch( (error) => { console.log('promise rejected: ', error); });
In general, you can handle erroneous HTTP status code by throwing an Error object. Next, you attach a ‘catch handler’ at the end of the chain of ‘then handlers’. The ‘catch handler’ will handle promise rejections and exceptions thrown in the ‘then handlers’.
const endpoint = '...'; fetch(endpoint).then( (response) => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }).then( (json) => { // process the json data console.log(json); }).catch( (error) => { console.log('promise rejected: ', error); });
Refer to https://css-tricks.com/using-fetch/ for more explanation on error handling in fetch.
fetch()
has a second parameter that allows you to set the HTTP verb (e.g. method: 'POST'
) and message body
. Notice that you need to format your data as a JSON string using JSON.stringify()
, and set the Content-type
header manually. The following example sends a POST
request to the testing endpoint to create a new post.
const content = { userId: 1, title: 'foo', body: 'bar' };
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify(content),
headers: {
'Content-type': 'application/json',
},
})
.then((response) => response.json())
.then((json) => console.log(json));
The code used to send PUT
, PATCH
and DELETE
are similar. You can find some examples in https://jsonplaceholder.typicode.com/.
If you need to send URL-encoded payload in request body, use
URLSearchParams
. For example,fetch('https://example.com/products.create', { method: 'POST', body: new URLSearchParams({ productName: "Great new chocolate", category: "Confections", unitPrice: 20, unitsInStock: 100 }) }).then(response => { // handle the response });
The sample program p540-fetch implements the client side of a web app. Download the project and uncompress the project. Install and run the project as follows.
$ npm install
# build the client side of the web app to ./dist
$ npm run build
# run the web api server
$ node server.mjs
You can now access the web app at http://localhost:8000/ . The web API is accessible at http://localhost:8000/api .
Fetch API
Testing APIs:
POST
, PUT
and DELETE
requests.