5 ways to make HTTP requests in JavaScript

Feature image

Modern Javascript provides a number of ways to send HTTP requests to remote servers. From the native XMLHttpRequest object to third-party libraries like Axios, having such a varied collection of picks has made requesting and dynamically loading content in web applications more effortless than ever before.

So, in today’s post, we’ll discuss different ways of sending HTTP requests in Javascript. Starting from the native options provided by the language, we’ll look at the following five modules and sending different types of HTTP requests with them.

Let’s get started!


XMLHttpRequest

XMLHttpRequest is a native API in Javascript that encapsulates the logic of sending HTTP requests without having to refresh a loaded web page (AJAX requests). Even though developers rarely use the XMLHttpRequest directly now, it’s still the building block that works underneath many popular HTTP request modules.

So, understanding how to send requests using the XMLHttpRequest method can help you handle unique use cases if a third-party library doesn’t support it.

Here’s how we can send GET requests and asynchronously retrieve data from a remote API using XMLHttpRequest API:

//create XMLHttpRequest object
const xhr = new XMLHttpRequest()
//open a get request with the remote server URL
xhr.open("GET", "https://world.openfoodfacts.org/category/pastas/1.json")
//send the Http request
xhr.send()

//EVENT HANDLERS

//triggered when the response is completed
xhr.onload = function() {
  if (xhr.status === 200) {
    //parse JSON datax`x
    data = JSON.parse(xhr.responseText)
    console.log(data.count)
    console.log(data.products)
  } else if (xhr.status === 404) {
    console.log("No records found")
  }
}

//triggered when a network-level error occurs with the request
xhr.onerror = function() {
  console.log("Network error occurred")
}

//triggered periodically as the client receives data
//used to monitor the progress of the request
xhr.onprogress = function(e) {
  if (e.lengthComputable) {
    console.log(`${e.loaded} B of ${e.total} B loaded!`)
  } else {
    console.log(`${e.loaded} B loaded!`)
  }
}

As this example shows, the process of sending a GET request with XMLHttpRequest involves three steps:

  • Create XMLHttpRequest
  • Opening the HTTP request of the indented type
  • Sending the request

Once the request is sent, we can use the event handlers provided by the XMLHttpObject to handle its response. Here, we have used two event handlers: onload, onerror, and onprogress. It’s important to note here that onerror method only handles network-level errors related to the request. To identify HTTP errors, we have to check the HTTP status code inside the onload method specifically.

We can send POST requests with XMLHttpRequest following a similar pattern.

// create XMLHttpRequest object
const xhr = new XMLHttpRequest()
// open a POST request
xhr.open("POST", "/food")
// set content-type header to JSON
xhr.setRequestHeader("Content-Type", "application/json");
// send JSON data to the remote server
xhr.send(JSON.stringify(food))

// Event Handlers

// track data upload progress
xhr.upload.onprogress = function(e) {
  console.log(`${e.loaded}B of ${e.total}B uploaded!`)
}

// triggered when data upload is finished
xhr.upload.onload = function(e) {
  console.log("Upload completed")
}

// triggered when the response is fully received
xhr.onload = function() {
  console.log(xhr.status)
}

// triggered due to a network-level error
xhr.onerror = function() {
  console.log("Network error occurred")
}

One main difference between the earlier GET and the current POST request is explicitly setting the content-type headers when posting JSON data. Also, there is an additional event type a POST request can trigger compared to a GET request. They are upload events accessed through the xhr.upload field. These event handlers help us track the data upload progress when the request body has to carry a significant amount of data (e.g., images, files, etc.)

Pros of XMLHttpRequest

  • Since the method is natively supported, it’s compatible with all modern browser versions.
  • Removes the need for external dependencies.
  • Allows accessing and manipulating asynchronous HTTP requests at the base level.

Cons of XMLHttpRequest

  • The code is verbose and unnecessarily long.
  • No support for async/await or promise-based syntax.
  • Most newer HTTP request packages provide simple abstractions over the complex XMLHttpRequest API.

Fetch

Fetch is a simplified and modern native Javascript API used for making HTTP requests. It comes with built-in support for promises and improves over the verbose syntax of the previously discussed XMLHttpRequest. As an API built with modern application and developer needs in mind, Fetch has become one of the most popular ways to send HTTP requests in Javascript today.

Following a promise-based syntax, we can use Fetch to send HTTP requests from the client-side, as this example shows.

fetch("https://world.openfoodfacts.org/category/pastas/1.json")
  .then(response => {
    // indicates whether the response is successful (status code 200-299) or not
    if (!response.ok) {
      throw new Error(`Request failed with status ${reponse.status}`)
    }
    return response.json()
  })
  .then(data => {
    console.log(data.count)
    console.log(data.products)
  })
  .catch(error => console.log(error))

Fetch significantly reduces the complexity and verboseness of the code with the use of simpler syntax and promises. In this implementation, we have to use the response.ok field to check whether the response contains an HTTP error or not because the errors caught in the catch method belong to the network level, not the application level.

The fetch method accepts a configuration object as the second parameter to allow easy manipulation of HTTP fields like headers, content-types, the request method, etc. You can find the complete list of configuration options Fetch supports in its official documentation .

Making POST requests with Fetch also follows a similar pattern to the previous example. Here, we use the config object to specify the request method and pass data that needs to be posted.

Let’s try this implementation using async/await:

async function postData () {
  const food = {
    name: "Bread",
    weight: 450,
    quantity: 3
  }

  const response = await fetch("/food", {
    method: "POST",
    body: JSON.stringify(food),
    headers: {
      "Content-Type": "application/json"
    }
  })

  if (!response.ok) {
    throw new Error(`Request failed with status ${reponse.status}`)
  }
  console.log("Request successful!")
}

Pros of Fetch

  • Provides a simplified, native way to make HTTP requests in Javascript.
  • Easy to learn and use for problems of any level.
  • Supports promise-based implementation, allowing us to write cleaner, concise code.
  • Provides additional features over XMLHttpRequest such as integrating Request and Response objects with the native Cache API and sending no-cors requests .

Cons of Fetch

  • Lacks some useful features supported by XMLHttpRequest such as aborting a request and monitoring request progress. (However, it allows the use of a separate AbortController object to control request aborts and timeouts.)
  • Accepts a response even when an HTTP error occurs. We have to manually check for HTTP errors and handle them.
  • Not compatible with Internet Explorer , though hopefully, this doesn’t matter anymore.

Axios

Axios is one of the most popular third-party packages used for making HTTP requests in Javascript. It works with the native XMLHttpRequest API under the hood to bring a convenient and versatile set of features for solving unique problems like intercepting HTTP requests and sending simultaneous requests. Similar to Fetch, it supports promises for handling asynchronous requests.

When making GET requests with Axios, we can use the dedicated axios.get() method to compile the request. Here we’ve shown an example of the implementation:

axios.get("https://world.openfoodfacts.org/category/pastas/1.json")
  .then(response => {
    // access parsed JSON response data using response.data field
    data = response.data
    console.log(data.count)
    console.log(data.products)
  })
  .catch(error => {
    if (error.response) {
      //get HTTP error code
      console.log(error.reponse.status)
    } else {
      console.log(error.message)
    }
  })

As this example shows, Axios reduces the amount of work we have to do on our end to make HTTP requests even compared to Fetch. It automatically parses the received JSON data, which we can access through response.data field. Axios also catches HTTP errors in its catch method, removing the need to specifically check for status code before processing the response. Inside the catch method, we can distinguish HTTP errors using an error.response check, which stores the HTTP error code.

For sending POST requests with Axios, we use the dedicated axios.post() method as the following example, implemented using async/await, shows:

async function postData () {
  const food = {
    name: "Bread",
    weight: 450,
    quantity: 3
  }

  try {
    const response = await axios.post("/food", food)
    console.log("Request successful!")
  } catch (error) {
    if (error.response) {
      console.log(error.reponse.status)
    } else {
      console.log(error.message)
    }
  }
}

Again, Axios simplifies this implementation by automatically converting Javascript objects to JSON without our interception. These Axios methods also accept a final parameter specifying HTTP configurations.

Other than these basic features, Axios provides solutions for many unique use cases that we won’t discuss here. But if you want to dive deeper into working with Axios in Javascript as well as Node.js, you can follow this in-depth guide to Axios on our blog.

Pros of Axios

  • Provides a simple, concise, and easy-to-learn syntax.
  • Supports a versatile set of features that aren’t available in many other available HTTP packages. These include intercepting HTTP requests, sending simultaneous requests, aborting sent requests, automatic JSON data transformation, monitoring request progress, etc.
  • Compatible with all main browser versions, including Internet Explorer.
  • Provides Client-side support for XSRF protection.

Cons of Axios

  • Adds an external dependency to the application since the module is not native.

SuperAgent

SuperAgent is one of the earliest third-party packages introduced to Javascript for making HTTP requests. Similar to Axios, it uses XMLHttpRequest API under the hood in its implementation and comes with a comprehensive set of features useful in a number of request handling tasks. The package supports both promise-based and callback-based implementations.

When sending HTTP requests with SuperAgent, we can rely on its dedicated methods to initiate a request of a particular type. For example, we can use the superagent.get() method to send GET requests, as this example shows.

superagent
  .get("https://world.openfoodfacts.org/category/pastas/1.json")
  .then(response => {
    // get parsed JSON response data
    data = response.body
    console.log(data.count)
    console.log(data.products)
  })
  .catch(error => {
    if (error.response) {
      console.log(error.status)
    } else {
      console.log(error.message)
    }
  })

With the promise-based syntax, SuperAgent follows a similar pattern to Axios for sending GET requests. It automatically parses the response body into a Javascript object without developer interference. It also catches HTTP errors inside the catch method, which we can identify using the error.response field. If the request fails due to a network-related error, these error.response and error.status fields will remain undefined.

We can send POST requests with SuperAgent in a similar way.

superagent
  .post("/food")
  .send(food)
  .then(response => console.log("Request successful!"))
  .catch(error => {
    if (error.response) {
      console.log(error.status)
    } else {
      console.log(error.message)
    }
  })

By default, SuperAgent assumes the passed data are in JSON and handles data transformation and sets content-type headers on its own. To pass the data sent with the POST request, we use SuperAgent’s send() method.

Pros of SuperAgent

  • Provides an easy-to-use, promise-based solution for sending HTTP requests.
  • It’s a mature and well-supported module in Javascript.
  • Supports retrying requests if a network-related or other transient error occurs when making a request.
  • Supports extending the package’s functionality with the help of a constantly evolving set of plugins . Some examples of features these plugins add to SuperAgent include simulating mock HTTP calls, caching request and response data, queueing and throttling requests, etc.
  • Compatible with all major browser versions. However, you have to use a polyfill for earlier versions of Internet Explorer to enable features like promise support, again, IE? who cares at this point, right?

Cons of SuperAgent

  • Adds an external dependency since the module is not native.
  • Doesn’t support monitoring request progress.

Ky

Ky is a relatively new Javascript package that can be used for making asynchronous HTTP requests from the front end of a web application. It’s built on top of the native Fetch API with a simpler syntax and additional functionality.

Ky provides a simple syntax for making requests with its dedicated HTTP methods. Here’s an example of sending a GET request using Ky with async/await.

async function getData () {
  try {
    const data = await ky.get("https://world.openfoodfacts.org/category/pastas/1.json").json()
    console.log(data.count)
    console.log(data.products)
  } catch (error) {
    if (error.response) {
      console.log(error.response.status)
    } else {
      console.log(error.message)
    }
  }
}

We can send POST requests following a similar pattern:

async function postData () {
  try {
    const response = await ky.post("/food", { json: food })
    console.log("Request successful!")
  } catch (error) {
    if (error.response) {
      console.log(error.reponse.status)
    } else {
      console.log(error.message)
    }
  }
}

Pros of Ky

  • Gives a simple, lightweight, promise-based API for making HTTP requests.
  • Addresses some limitations in the native Fetch API with support for features like request timeout, retry, and monitoring progress.
  • Provides hooks for modifying requests during their lifecycle: beforeRequest, afterResponse, beforeRetry, etc.
  • Supports all modern browsers like Chrome, Firefox, Safari. For Internet Explorer support, Ky provides an alternative package, Ky-Universal , not sure why they still bother.

Cons of Ky

  • Relatively a new package compared to other mature, versatile options discussed in this post. Adds an external dependency.

Wrapping Up

In recent years, a number of native and third-party modules have been introduced to Javascript for the purpose of sending HTTP requests. In the five methods we discussed today, we touched on traditional, popular, and even relatively new ways of accomplishing this task to give a complete overview of different options available to you as a developer.

While these methods have their own strengths and weaknesses, you can pick the best fit to use in your web applications after carefully considering your requirements. We hope this post will help you conduct that analysis and identify the right method for sending HTTP requests in your future projects.