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.
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 athttps://domainA.com/
, such ashttps://domainA.com/api/
orhttps://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. -
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, settingdefault-src 'self'
stopsfetch()
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 thefetch()
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.