HTTP Requests in JavaScript


Reading time: 40 minutes | Coding time: 15 minutes

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 "www.google.com" and getting the Google 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 the Mozilla Developer Network website, 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. Whether you are loading a page or updating it, these requests are quite useful.

JavaScript has a set of great tools and methods that allow us to make HTTP requests whether it is 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(). The prefix 'XML' doesn't determine the type of data we can receive from this type of request. We can receive any form of data.

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:

  1. Method: GET, POST, PUT or DELETE,
  2. URL: Path to our resource,
  3. Async: Boolean value (true or false); Optional & default value is true,
  4. 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 the 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 like Content-Type & Accept. Similarly, we can use xhr.getResponseHeader() to get header values for 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. 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 value
  • json
  • document
  • blob
xhr.responseType = 'json';

xhr.readyState

Our request changes states as it progresses and the current state of the request can be accessed by this property. It's 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 has their request 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 our 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. I chose 'posts' for this demo. 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 resURL = "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", resURL, 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 resURL = "https://jsonplaceholder.typicode.com/posts";

// 1. CREATE OUR XHR OBJECT
const xhr = new XMLHttpRequest();

// 2. Configure a `POST` request
xhr.open('POST', resURL);

// 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 this (JSONPlaceholder) 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 resURL = "https://jsonplaceholder.typicode.com/posts/1";

// Configure a `PUT` request
xhr.open('PUT', resURL);

// 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 resURL = "https://jsonplaceholder.typicode.com/posts/1";

// 2. CREATE OUR XHR OBJECT
const xhr = new XMLHttpRequest();

// 3. Configure a `DELETE` request
xhr.open('DELETE', resURL);

// 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 details how the fetch() API works here. But let's redo the previous requests using fetch and see the difference.

Method: GET

let resURL = "https://jsonplaceholder.typicode.com/posts";

fetch(resURL)
.then(res => res.json())
.then(data => console.log(data))
.catch(error => console.log(error));

Method: POST

fetch(resURL, {
    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 resURL = "https://jsonplaceholder.typicode.com/posts/1";

fetch(resURL, {
    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 resURL = "https://jsonplaceholder.typicode.com/posts/1";

fetch(resURL, {
    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 some other tools that require the installation of a package (like the axios() library) or some that come as part of a language framework (like $.ajax() from jQuery). These utilize XHR and/or Fetch API under the hood to provide a much cleaner way of making requests for developers.

References