Making requests is the heart of the Internet. Whenever you are browsing the internet, all what you are doing is requesting information from servers, which are basically interconnected computers. It's the foundation of how websites work. A simple case is a person typing "duckduckgo.com" and getting the DuckDuckGo search page. A request is made to a web server, which then sends back the files for that page, which will be rendered in the browser as a web page. This is made possible by a protocol known as HTTP (HyperText Transfer Protocol). On MDN web docs, the HTTP protocol is described as:
HTTP is a protocol which allows the fetching of resources, such as HTML documents. It is the foundation of any data exchange on the Web, and it is a client-server protocol, which means requests are initiated by the recipient, usually the Web browser. A complete document is reconstructed from the different sub-documents fetched, for instance text, layout description, images, videos, scripts, and more.
Requesting a resource from the client side uses this HTTP protocol, and these types of requests are known as HTTP requests. These requests are essential for any type of online activity performed on the web.
JavaScript has a set of great tools and methods that allow us to make HTTP requests to send or receive data from a certain server or endpoint. A couple of commonly used ways to make requests are XMLHttpRequest and Fetch.
1. XMLHttpRequest (XHR)
AJAX stands for Asynchronous JavaScript And XML. Requests made are asynchronous, which means they don't interrupt the execution of other JavaScript code. In other words, JavaScript code besides the AJAX code doesn't have to wait for requests to finish being executed. It can execute simultaneously. The method that makes this possible is the XMLHttpRequest()
. Despite the prefix 'XML', we can process any form of data for our requests.
The XMLHttpRequest()
method works as a constructor for us to create an instance and call our AJAX requests on.
const xhr = new XMLHttpRequest();
Our xhr
object has a number of methods and properties that allow us to make different types of requests.
Methods
xhr.open()
We can then use the open()
method on our xhr
object to initialize or configure a request. This method takes the following parameters:
- Method: GET, POST, PUT or DELETE,
- URL: Path to our resource,
- Async: Boolean value (
true
orfalse
); Optional & default value istrue
, - Username & Password — Optional; Credentials if authentication is needed.
It follows the syntax:
xhr.open(Method, URL[, Async]);
xhr.send()
This method allows us to open and send our request. It takes an optional parameter that contains our request body, which is useful when making POST requests.
xhr.send([body]);
xhr.setRequestHeader()
This method is used to set HTTP headers for our request, such as Content-Type
& Accept
. Similarly, we can use xhr.getResponseHeader()
to get header values from our response.
// Set header for request
xhr.setRequestHeader("Content-Type", "application/json");
// Get header for response
xhr.getResponseHeader("Content-Type");
xhr.abort()
This method is useful when we want to cancel a request. Imagine a scenario where a user wants to cancel a request because it's taking too long. xhr.abort()
can be used to stop the request.
xhr.abort();
Properties
xhr.response
This value contains the response body we receive from our request. xhr.responseText
is also a similar property, which is used if our response is in a string format.
const res = xhr.response;
xhr.responseType
This is used to set the response type for our xhr
object. It can be in any one of these formats:
text
/""
- default valuejson
document
blob
xhr.responseType = "json";
xhr.readyState
Our request changes states as it progresses, and the current state of our request can be accessed by this property. Its values range from 0 (initial state) to 4 (completed state). The event onreadystatechange
can be used to detect whenever there is a change in state.
xhr.onreadystatechange = function () {
if (xhr.readyState == 1) {
/* request opened */
}
if (xhr.readyState == 2) {
/* headers received */
}
if (xhr.readyState == 3) {
/* response loading */
}
if (xhr.readyState == 4) {
/* request complete */
}
};
Events
xhr.onload
This event is fired when our request is successful and the result is sent back. This can also be computed with xhr.onreadystatechange
when xhr.readyState
is 4.
xhr.onload = () => {
// Code to run when results are ready
};
xhr.onerror
This event is invoked when there is an error in our request, which could be due to network error or invalid configuration of our request.
xhr.onerror = () => {
// Code to run when request has failed
};
xhr.onprogress
This event is called repeatedly while our request is processed. It can be used to figure out the loading progress of a download or even a page load to let the user know how far along their request has been processed. An event parameter can be passed to xhr.onprogress
and it has properties event.loaded
and event.total
, which can be used to show the actual progress feedback.
xhr.onprogress = (event) => {
// Code to run while request is being processed
console.log(`Loaded ${event.loaded} of ${event.total}`);
};
Example
We are going to use a simple fake online REST API called JSONPlaceholder to make example requests. We will use it to request and submit fake posts. According to the documentation for JSONPlaceholder, our path to the resource we want is https://jsonplaceholder.typicode.com/<endpoint>
, and there are several endpoints to choose from. For this demo, we'll use 'posts' as our endpoint. For this certain endpoint, we are going to receive and submit to an array of posts which are enclosed in objects. We also want our request to be asynchronous.
Method: GET - is used to retrieve data.
// Our path to the resource
let reqURL = "https://jsonplaceholder.typicode.com/posts";
// 1. CREATE OUR XHR OBJECT
const xhr = new XMLHttpRequest();
// 2. SET OUR RESPONSE TYPE
xhr.responseType = "json";
// 3. CONFIGURE OUR REQUEST
xhr.open("GET", reqURL, true);
// 4. SEND THE REQUEST
xhr.send();
// 5. DECLARE FUNCTIONS BASED ON EVENTS
xhr.onload = () => {
const data = xhr.response;
console.log(data);
};
xhr.onprogress = (event) => {
console.log(`Loaded ${event.loaded} of ${event.total}`);
};
xhr.onerror = () => {
console.log("Request failed!");
};
As an output, we get a list of objects in the form of an array.
// An example object from the array we get
{
id: 1,
title: "sunt aut facere repellat provident occaecati excepturi",
body: "quia et suscipit suscipit recusandae consequuntur expedita et cum reprehenderit ...",
userId: 1
}
Method: POST - is used to send new or updated data to the specified resource.
// Our path to the resource
let reqUrl = "https://jsonplaceholder.typicode.com/posts";
// 1. CREATE OUR XHR OBJECT
const xhr = new XMLHttpRequest();
// 2. Configure a `POST` request
xhr.open("POST", reqUrl);
// 3. Create a JSON 'post' object
const json_data = {
title: "this is title",
body: "this is body",
userId: 1
};
// 4. Set the `Content-Type` header
xhr.setRequestHeader("Content-Type", "application/json");
// 5. Pass a string form of our
// json_data object to `send()`
xhr.send(JSON.stringify(json_data));
// 6. DECLARE FUNCTIONS BASED ON EVENTS
xhr.onload = () => {
const data = xhr.response;
console.log(data);
};
xhr.onprogress = (event) => {
console.log(`Uploaded ${event.loaded} of ${event.total}`);
};
xhr.onerror = () => {
console.log("Request failed!");
};
Other than the couple of steps we have added, the general structure and process of making HTTP requests is very similar and perhaps even almost identical.
For our demo API, the object we 'posted' is not actually added to their database of posts. Since it is a fake API, the process is also faked, as the purpose of this code and the API is to demonstrate how requests work and to test code.
Method: PUT - is used to update or modify data at the specified path. This is almost identical to our POST method process. The only things that change are the path, the method name and the object we are updating. Other than these values, the rest is the same.
// 1. Our path to the object we want to update
// Notice the '/1' added to the end
let reqUrl = "https://jsonplaceholder.typicode.com/posts/1";
// Configure a `PUT` request
xhr.open('PUT', reqUrl);
// Create a JSON 'post' object
// We added 'id' so it can be used to
// update the entry with the specified id
const json_data = {
id: 1
title: 'this is updated title',
body: 'this is updated body',
userId: 1
};
Method: DELETE - is used to delete the specified resource. Based on the documentation for the API, we use a different path to our resource to target and delete an entry.
// 1. Our path to the object we want to delete
// Notice the '/1' added to the end
let reqUrl = "https://jsonplaceholder.typicode.com/posts/1";
// 2. CREATE OUR XHR OBJECT
const xhr = new XMLHttpRequest();
// 3. Configure a `DELETE` request
xhr.open("DELETE", reqUrl);
// 4. SEND THE REQUEST
xhr.send();
For real APIs, we can continue to create another GET request to verify that the data we wanted to delete is actually deleted. We can retrieve all the data and see if the object we sent a DELETE request on is in the database. If it's not, then the data is deleted.
2. Fetch
Fetch is a promise-based web API. It is a modern alternative to XHR. I have explained in detail how the fetch()
API works here. But let's redo the previous requests using fetch and see the difference.
Method: GET
let reqUrl = "https://jsonplaceholder.typicode.com/posts";
fetch(reqUrl)
.then((res) => res.json())
.then((data) => console.log(data))
.catch((error) => console.log(error));
Method: POST
fetch(reqUrl, {
method: "POST",
body: JSON.stringify({
title: "this is title",
body: "this is body",
userId: 1
}),
headers: {
"Content-type": "application/json"
}
})
.then((res) => res.json())
.then((data) => console.log(data))
.catch((error) => console.log(error));
Method: PUT
// Our path to the object we want to update
let reqUrl = "https://jsonplaceholder.typicode.com/posts/1";
fetch(reqUrl, {
method: "PUT",
body: JSON.stringify({
id: 1,
title: "this is title",
body: "this is body",
userId: 1
}),
headers: {
"Content-type": "application/json"
}
})
.then((res) => res.json())
.then((data) => console.log(data))
.catch((error) => console.log(error));
Method: DELETE
// 1. Our path to the object we want to delete
let reqUrl = "https://jsonplaceholder.typicode.com/posts/1";
fetch(reqUrl, {
method: "DELETE"
});
Fetch is a much cleaner and simpler way of doing what XHR allows us to do. Tracking progress is not yet supported in fetch. Due to that and many other reasons like browser support, XHR is still an important part of making requests.
There are also third-party tools such as axios
and jQuery's $.ajax()
. These utilize XHR and/or Fetch API under the hood to provide a much cleaner way of making requests for developers.