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

    Craig Buckler
    Share

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

    Table of Contents

    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.