How to use the Fetch API in Node.js, Deno, and Bun

Share this article

How to use the Fetch API in Node.js, Deno, and Bun

In this article, we’ll look at how to use the Fetch API with Node.js, Deno, and Bun.

Table of Contents

Key Takeaways

  • Fetch API in Modern JavaScript Environments: This article discusses how to use the Fetch API, a modern and simpler alternative to XMLHttpRequest, across various JavaScript environments like Node.js, Deno, and Bun, focusing on its promise-based structure and ease of use.
  • Differences in Fetch Usage Client-Side vs Server-Side: While the Fetch API provides a consistent interface across client and server environments, the article highlights key differences in usage constraints, such as CORS and CSP on the client side, and fewer restrictions but potential third-party API limitations on the server side.
  • Effective Fetch Request Strategies: The article emphasizes the importance of efficient fetch request strategies like using Promise.allSettled for concurrent requests and managing timeouts with AbortController, ensuring optimized performance and better error handling in web applications.

Fetch API vs XMLHttpRequest

Fetching data via an HTTP request is fundamental web application activity. You may have made such calls in the browser, but the Fetch API is natively supported in Node.js, Deno, and Bun.

In a browser, you might request information from a server so you can display it without a full screen refresh. This is typically known as an Ajax request or a single page application (SPA). Between 1999 and 2015, XMLHttpRequest was the only option — and remains so if you want to show file upload progress. XMLHttpRequest is a fairly clunky callback-based API, but it permits fine-grained control and, despite the name, it’ll handle responses in formats other than XML — such as text, binary, JSON, and HTML.

Browsers have implemented the Fetch API from 2015. It’s a simpler, easier, more consistent, promise-based alternative to XMLHttpRequest.

Your server-side code may also want to make HTTP requests — typically to call APIs on other servers. From their first release, both the Deno and Bun runtimes usefully replicated the browser’s Fetch API so that similar code could run on both the client and server. Node.js required a third-party module such as node-fetch or axios until February 2022, when version 18 added the standard Fetch API. It’s still considered experimental, but you can now use fetch() everywhere with identical code in most cases.

A Basic Fetch Example

This simple example fetches response data from a URI:

const response = await fetch('https://example.com/data.json');

The fetch() call returns a promise which resolves with a Response object providing information about the result. You can parse the HTTP response body into a JavaScript object using the promise-based .json() method:

const data = await response.json();

// do something exciting with the data object
// ...

Client-side vs Server-side Fetch

The API may be identical across platforms, but browsers enforce restrictions when making client-side fetch() requests:

  • Cross-origin resource sharing (CORS)

    Client-side JavaScript can only communicate with API endpoints within its own domain. A script loaded from https://domainA.com/js/main.js can call any service at https://domainA.com/, such as https://domainA.com/api/ or https://domainA.com/data/.

    It’s impossible to call a service on https://domainB.com/ — unless that server permits access by setting an HTTP Access-Control-Allow-Origin header.

  • Content Security Policy (CSP)

    Your web sites/apps can set a Content-Security-Policy HTTP header or meta tag to control permitted assets in a page. It can prevent accidental or malicious injection of scripts, iframes, fonts, images, videos, and so on. For example, setting default-src 'self' stops fetch() requesting data outside its own domain (XMLHttpRequest, WebSocket, server-sent events, and beacons are also restricted).

Server-side Fetch API calls in Node.js, Deno, and Bun have fewer restrictions, and you can request data from any server. That said, third-party APIs may:

  • require some sort of authentication or authorization using keys or OAuth
  • have maximum request thresholds, such as no more than one call per minute, or
  • make a commercial charge for access

You can use server-side fetch() calls to proxy client-side requests so you can avoid CORS and CSP issues. That said, remember to be a conscientious web citizen and don’t bombard services with thousands of requests that could take them down!

Custom Fetch Requests

The example above requests data from the URI https://example.com/data.json. Below the surface, JavaScript creates a Request object, which represents the full details of that request such as the method, headers, body, and more.

fetch() accepts two arguments:

  • the resource – a string or URL object, and
  • an optional options parameter with further request settings

For example:

const response = await fetch('https://example.com/data.json', {
   method: 'GET',
   credentials: 'omit',
   redirect: 'error',
   priority: 'high'
});

The options object can set following properties in Node.js or client-side code:

property values
method GET (the default), POST, PUT, PATCH, DELETE, or HEAD
headers a string or Headers object
body can be a string, JSON, blob, etc.
mode same-origin, no-cors, or cors
credentials omit, same-origin, or include cookies and HTTP authentication headers
redirect follow, error, or manual handling of redirects
referrer the referring URL
integrity subresource integrity hash
signal an AbortSignal object to cancel the request

Optionally, you can create a Request object and pass it to fetch(). This may be practical if you can define API endpoints in advance or want to send a series similar requests:

const request = new Request('https://example.com/api/', {
  method: 'POST',
  body: '{"a": 1, "b": 2, "c": 3}',
  credentials: 'omit'
});

console.log(`fetching ${ request.url }`);
const response = await fetch(request);

Handling HTTP Headers

You can manipulate and examine HTTP headers in the request and response using a Headers object. The API will be familiar if you’ve used JavaScript Maps:

// set inital headers
const headers = new Headers({
  'Content-Type': 'text/plain',
});

// add header
headers.append('Authorization', 'Basic abc123');

// add/change header
headers.set('Content-Type', 'application/json');

// get a header
const type = headers.get('Content-Type');

// has a header?
if (headers.has('Authorization')) {

   // delete a header
   headers.delete('Authorization');

}

// loop through all headers
headers.forEach((value, name) => {
  console.log(`${ name }: ${ value }`);
});

// use in fetch()
const response = await fetch('https://example.com/data.json', {
   method: 'GET',
   headers
});

// response.headers also returns a Headers object
response.headers.forEach((value, name) => {
  console.log(`${ name }: ${ value }`);
});

Fetch Promise Resolve and Reject

You might presume a fetch() promise will reject when an endpoint returns a 404 Not Found or similar server error. It doesn’t! The promise will resolve, because that call was successful — even if the result wasn’t what you expected.

A fetch() promise only rejects when:

  • you make an invalid request — such as fetch('httttps://!invalid\URL/');
  • you abort the fetch() request, or
  • there’s a network error, such as a connection failure

Analyzing Fetch Responses

Successful fetch() calls return a Response object containing information about the state and returned data. The properties are:

property description
ok true if the response was successful
status the HTTP status code, such as 200 for success
statusText the HTTP status text, such as OK for a 200 code
url the URL
redirected true if the request was redirected
type the response type: basic, cors, error, opaque, or opaqueredirect
headers the response Headers object
body a ReadableStream of body content (or null)
bodyUsed true if the body has been read

The following Response object methods all return a promise, so you should use await or .then blocks:

method description
text() returns the body as a string
json() parses the body to a JavaScript object
arrayBuffer() returns the body as an ArrayBuffer
blob() returns the body as a Blob
formData() returns the body as a FormData object of key/value pairs
clone() clones the response, typically so you can parse the body in different ways
// example response
const response = await fetch('https://example.com/data.json');

// response returned JSON?
if ( response.ok && response.headers.get('Content-Type') === 'application/json') {

   // parse JSON
   const obj = await response.json();

}

Aborting Fetch Requests

Node.js won’t time out a fetch() request; it could run forever! Browsers can also wait between one and five minutes. You should abort fetch() under normal circumstances where you’re expecting a reasonably quick response.

The following example uses an AbortController object, which passes a signal property to the second fetch() parameter. A timeout runs the .abort() method if fetch doesn’t complete within five seconds:

// create AbortController to timeout after 5 seconds
const
  controller = new AbortController(),
  signal = controller.signal,
  timeout = setTimeout(() => controller.abort(), 5000);

try {

  const response = await fetch('https://example.com/slowrequest/', { signal });

  clearTimeout(timeout);

  console.log( response.ok );

}
catch (err) {

  // timeout or network error
  console.log(err);

}

Node.js, Deno, Bun, and most browsers released since mid-2022 also support AbortSignal. This offers a simpler timeout() method so you don’t have to manage your own timers:

try {

  // timeout after 5 seconds
  const response = await fetch('https://example.com/slowrequest/', {
    signal: AbortSignal.timeout( 5000 ),
  });

  console.log( response.ok );

}
catch (err) {

  // timeout or network error
  console.log(err);

}

Effective Fetches

Like any asynchronous, promise-based operation, you should only make fetch() calls in series when the input of a call depends on the output of a previous one. The following code doesn’t perform as well as it could because each API call must wait for the previous one to resolve or reject. If each response takes one second, it’ll take a total of three seconds to complete:

// inefficent
const response1 = await fetch('https://example1.com/api/');
const response2 = await fetch('https://example2.com/api/');
const response3 = await fetch('https://example3.com/api/');

The Promise.allSettled() method runs promises concurrently and fulfills when all have resolved or rejected. This code completes at the speed of the slowest response. It will be three times faster:

const data = await Promise.allSettled(
  [
    'https://example1.com/api/',
    'https://example2.com/api/',
    'https://example3.com/api/'
  ].map(url => fetch( url ))
);

data returns an array of objects where:

  • each has a status property string of "fullfilled" or "rejected"
  • if resolved, a value property returns the fetch() response
  • if rejected, a reason property returns the error

Summary

Unless you’re using a legacy version of Node.js (17 or below), the Fetch API is available in JavaScript on both the server and client. It’s flexible, easy to use, and consistent across all runtimes. A third-party module should only be necessary if you require more advanced functionality such as caching, retries, or file handling.

Craig BucklerCraig Buckler
View Author

Craig is a freelance UK web consultant who built his first page for IE2.0 in 1995. Since that time he's been advocating standards, accessibility, and best-practice HTML5 techniques. He's created enterprise specifications, websites and online applications for companies and organisations including the UK Parliament, the European Parliament, the Department of Energy & Climate Change, Microsoft, and more. He's written more than 1,000 articles for SitePoint and you can find him @craigbuckler.

bunDenoFetch APInode.js
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week