Page
You may not need Axios
Fetch API to the rescue!
updated 1 day ago
You may not need Axios
This is not an attack on Axios.
Rather, it’s advocacy for thefetch
API which has become quite capable. 🦄
Overview
This article is a collection of the “missing” fetch
code snippets and common use cases I wish were easier to find.
Fetch Snippets
My top 10 list of fetch
examples, plus a bonus or 2.
- Get JSON from a URL
- Custom headers
- HTTP Error Handling
- CORS example
- Posting JSON
- Posting an HTML
<form>
- Form encoded data
- Uploading a file
- Uploading multiple files
- Timeouts
- Download Progress Helper
- Recursive Retry Helper
- Handling HTTP Redirects
Is your Use Case not listed? Let me know ✉️
Feature Comparison
fetch | axios | request | |
---|---|---|---|
Intercept request and response | ✅ | ✅ | ✅ |
Transform request and response data | ✅ | ✅ | ✅ |
Cancel requests | ✅ | ✅ | ❌ |
Automatic transforms for JSON data | ✅ | ✅ | ✅ |
Client side support for protecting against XSRF | ✅ | ✅ | ✅ |
Progress | ✅ | ✅ | ✅ |
Streaming | ✅ | ✅ | ✅ |
Redirects | ✅ | ✅ | ✅ |
When starting this article (late 2018, updated 2022) I assumed I’d end with a table of mixed check boxes. Surely there are special Use Cases which justified axios
, request
, r2
, superagent
, got
, etc.
Well, as it turns out, I overestimated the need for 3rd party http libraries.
Despite using fetch
for several years (including for non-trivial tasks: file uploads & error/retry support) I still had misconceptions of fetch
’s abilities and limits.)
Well, let’s check out what fetch
can do…
Get JSON from a URL
fetch('https://api.github.com/orgs/nodejs') | |
.then(response => response.json()) | |
.then(data => { | |
console.log(data) // result from `response.json()` above | |
}) | |
.catch(error => console.error(error)) |
Custom headers
fetch('https://api.github.com/orgs/nodejs', { | |
headers: new Headers({ | |
'User-agent': 'Mozilla/4.0 Custom User Agent' | |
}) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
console.log(data) | |
}) | |
.catch(error => console.error(error)) |
HTTP Error Handling
const isOk = response => response.ok ? response.json() : Promise.reject(new Error('Failed to load data from server')) | |
fetch('https://api.github.com/orgs/nodejs') | |
.then(isOk) // <= Use `isOk` function here | |
.then(data => { | |
console.log(data) // Prints result from `response.json()` | |
}) | |
.catch(error => console.error(error)) |
CORS example
CORS is primarily checked at the server - so make sure your configuration is correct on the server-side.
The credentials
option controls if your cookies are automatically included.
fetch('https://api.github.com/orgs/nodejs', { | |
credentials: 'include', // Useful for including session ID (and, IIRC, authorization headers) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
console.log(data) // Prints result from `response.json()` | |
}) | |
.catch(error => console.error(error)) |
Posting JSON
postRequest('http://example.com/api/v1/users', {user: 'Dan'}) | |
.then(data => console.log(data)) // Result from the `response.json()` call | |
function postRequest(url, data) { | |
return fetch(url, { | |
credentials: 'same-origin', // 'include', default: 'omit' | |
method: 'POST', // 'GET', 'PUT', 'DELETE', etc. | |
body: JSON.stringify(data), // Use correct payload (matching 'Content-Type') | |
headers: { 'Content-Type': 'application/json' }, | |
}) | |
.then(response => response.json()) | |
.catch(error => console.error(error)) | |
} |
<form>
Posting an HTML postForm('http://example.com/api/v1/users', 'form#userEdit') | |
.then(data => console.log(data)) | |
function postForm(url, formSelector) { | |
const formData = new FormData(document.querySelector(formSelector)) | |
return fetch(url, { | |
method: 'POST', // 'GET', 'PUT', 'DELETE', etc. | |
body: formData // a FormData will automatically set the 'Content-Type' | |
}) | |
.then(response => response.json()) | |
.catch(error => console.error(error)) | |
} |
Form encoded data
To post data with a Content-Type of application/x-www-form-urlencoded
we will use URLSearchParams
to encode the data like a query string.
For example, new URLSearchParams({a: 1, b: 2})
yields a=1&b=2
.
postFormData('http://example.com/api/v1/users', {user: 'Mary'}) | |
.then(data => console.log(data)) | |
function postFormData(url, data) { | |
return fetch(url, { | |
method: 'POST', // 'GET', 'PUT', 'DELETE', etc. | |
body: new URLSearchParams(data), | |
headers: new Headers({ | |
'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8' | |
}) | |
}) | |
.then(response => response.json()) | |
.catch(error => console.error(error)) | |
} |
Uploading a file
postFile('http://example.com/api/v1/users', 'input[type="file"].avatar') | |
.then(data => console.log(data)) | |
function postFile(url, fileSelector) { | |
const formData = new FormData() | |
const fileField = document.querySelector(fileSelector) | |
formData.append('username', 'abc123') | |
formData.append('avatar', fileField.files[0]) | |
return fetch(url, { | |
method: 'POST', // 'GET', 'PUT', 'DELETE', etc. | |
body: formData // Coordinate the body type with 'Content-Type' | |
}) | |
.then(response => response.json()) | |
.catch(error => console.error(error)) | |
} |
Uploading multiple files
Setup a file upload element with the multiple
attribute:
<input type='file' multiple class='files' name='files' /> |
Then use with something like:
postFile('http://example.com/api/v1/users', 'input[type="file"].files') | |
.then(data => console.log(data)) | |
function postFile(url, fileSelector) { | |
const formData = new FormData() | |
const fileFields = document.querySelectorAll(fileSelector) | |
// Add all files to formData | |
Array.prototype.forEach.call(fileFields.files, f => formData.append('files', f)) | |
// Alternatively for PHPeeps, use `files[]` for the name to support arrays | |
// Array.prototype.forEach.call(fileFields.files, f => formData.append('files[]', f)) | |
return fetch(url, { | |
method: 'POST', // 'GET', 'PUT', 'DELETE', etc. | |
body: formData // Coordinate the body type with 'Content-Type' | |
}) | |
.then(response => response.json()) | |
.catch(error => console.error(error)) | |
} |
Timeouts
Here’s a generic Promise timeout, using the “Partial Application” pattern. It’ll work with any Promise interface. Don’t do too much work in the supplied promise chain, it will keep running - and any failures have a way of creating long term memory leaks.
function promiseTimeout(msec) { | |
return promise => { | |
const timeout = new Promise((yea, nah) => setTimeout(() => nah(new Error('Timeout expired')), msec)) | |
return Promise.race([promise, timeout]) | |
} | |
} | |
promiseTimeout(5000)(fetch('https://api.github.com/orgs/nodejs')) | |
.then(response => response.json()) | |
.then(data => { | |
console.log(data) // Prints result from `response.json()` in getRequest | |
}) | |
.catch(error => console.error(error)) // Catches any timeout (or other failure) | |
//////////////////////////////////////////////////////////////////////// | |
//////////////////////////////////////////////////////////////////////// | |
//////////////////////////////////////////////////////////////////////// | |
// Alternative example: | |
fetchTimeout(5000, 'https://api.github.com/orgs/nodejs') | |
.then(console.log) | |
// Alternative implementation: | |
function fetchTimeout(msec, ...args) { | |
return raceTimeout(fetch(...args)) | |
function raceTimeout(promise) { | |
const timeout = new Promise((yea, nah) => setTimeout(() => nah(new Error('Timeout expired')), msec)) | |
return Promise.race([promise, timeout]) | |
} | |
} |
And a more complex example, featuring a tracking flag __timeout
so you can intercept any costly work.
function promiseTimeout(msec) { | |
return (promise) => { | |
let isDone = false | |
promise.then(() => isDone = true) | |
const timeout = new Promise((yea, nah) => setTimeout(() => { | |
if (!isDone) { | |
promise.__timeout = true | |
nah(new Error('Timeout expired')) | |
} | |
}, msec)) | |
return Promise.race([promise, timeout]) | |
} | |
} | |
promiseTimeout(5000)(fetch('https://api.github.com/orgs/nodejs')) | |
.then(response => response.json()) | |
.then(data => { | |
console.log(data) // Prints result from `response.json()` in getRequest | |
}) | |
.catch(error => console.error(error)) |
Download Progress Helper
Upload Progress is currently a bit buggy outside of Chrome.
The Progress Handler technique shown below avoids wrapping the fetch
call in a closure. 👍
progressHelper
has the following interface (source available below)
const progressHelper = require('./progressHelper.js') | |
const handler = ({loaded, total}) => { | |
console.log(`Downloaded ${loaded} of ${total}` ) | |
} | |
// handler args: ({ loaded = Kb, total = 0-100% }) | |
const streamProcessor = progressHelper(handler) | |
// => streamProcessor is a function for use with the response _stream_ |
Let’s look at a usage example:
// The progressHelper could be inline w/ .then() below... | |
const streamProcessor = progressHelper(console.log) | |
fetch('https://fetch-progress.anthum.com/20kbps/images/sunrise-progressive.jpg') | |
.then(streamProcessor) // note: NO parentheses because `.then` needs to get a function | |
.then(response => response.blob()) | |
.then(blobData => { | |
// ... set as base64 on an <img src="base64..."> | |
}) |
A reusable image downloader might look like getBlob()
:
const getBlob = url => fetch(url) | |
.then(progressHelper(console.log)) // progressHelper used inside the .then() | |
.then(response => response.blob()) |
By the way, a Blob
is a Binary Large Object.
It’s important to choose ONE of the 2 usage patterns below (they are functionally equivalent):
// OPTION #1: no temp streamProcessor var | |
fetch(...) | |
.then(progressHelper(console.log)) | |
// ⚠️ OR️ ️⚠️ | |
// OPTION #2: define a `streamProcessor` to hold our console logger | |
const streamProcessor = progressHelper(console.log) | |
fetch(...) | |
.then(streamProcessor) |
My preference is Option #1
. However, your scope design may force you to use Option #2
.
Finally, here’s the last part of this recipe, our progressHelper
:
Source: Progress Helper
function progressHelper(onProgress) { | |
return (response) => { | |
if (!response.body) return response | |
let loaded = 0 | |
const contentLength = response.headers.get('content-length') | |
const total = !contentLength ? -1 : parseInt(contentLength, 10) | |
return new Response( | |
new ReadableStream({ | |
start(controller) { | |
const reader = response.body.getReader() | |
return read() | |
function read() { | |
return reader.read() | |
.then(({ done, value }) => { | |
if (done) return void controller.close() | |
loaded += value.byteLength | |
onProgress({ loaded, total }) | |
controller.enqueue(value) | |
return read() | |
}) | |
.catch(error => { | |
console.error(error) | |
controller.error(error) | |
}) | |
} | |
} | |
}) | |
) | |
} | |
} |
credit: Special thanks to Anthum Chris and his fantastic Progress+Fetch PoC shown here
Recursive Retry Helper
/** | |
* A **Smarter** retry wrapper with currying! | |
*/ | |
function retryCurry(fn, retriesLeft = 5) { | |
const retryFn = (...args) => fn(...args) | |
.catch(err => retriesLeft > 0 | |
? retryFn(fn, retriesLeft - 1) | |
: Promise.reject(err) | |
}) | |
return retryFn | |
} | |
const getJson = (url) => fetch(url) | |
.then(response => response.json()) | |
// Usage | |
const retryGetJson = retryCurry(getJson, 3); | |
// Now you can pass any arguments through to your function! | |
retryGetJson('https://api.github.com/orgs/elite-libs') | |
.then(console.log) | |
.catch(console.error) |
/** Basic retry wrapper for Promises */ | |
function retryPromise(fn, retriesLeft = 5) { | |
return fn() | |
.catch(err => retriesLeft > 0 | |
? retryPromise(fn, retriesLeft - 1) | |
: Promise.reject(err) | |
}) | |
} | |
const getJson = (url) => fetch(url) | |
.then(response => response.json()) | |
// Usage | |
retry(() => getJson('https://api.github.com/orgs/elite-libs')) | |
.then(console.log) | |
.catch(console.error) |
Handling HTTP Redirects
const checkForRedirect = (response) => { | |
// Check for temporary redirect (307), or permanent (308) | |
if (response.status === 307 || response.status === 308) { | |
const location = response.headers.get('location') | |
if (!location) { | |
return Promise.reject(new Error('Invalid HTTP Redirect! No Location header.')); | |
} | |
// You can change the behavior here to any custom logic: | |
// e.g. open a "confirm" modal, log the redirect url, etc. | |
return fetch(location) | |
// Bonus: this will handle recursive redirects ✨ | |
.then(checkForRedirect) | |
} | |
return response | |
}; | |
fetch('https://api.github.com/orgs/elite-libs') | |
// Next line will handle redirects | |
.then(checkForRedirect) | |
.then(response => response.json()) | |
.then(console.log) | |
.catch(console.error) |
Compatibility
As of 2022, the fetch
API is widely supported in all modern browsers and in more recent versions of NodeJS v18+.
If you must support IE you can polyfill fetch with the github/fetch
package (maintained by an awesome team at GitHub). It’s possible to go as far back as IE8 - Your mileage may vary.
Earlier NodeJS can take advantage of the the fetch
API with the node-fetch
package:
npm install node-fetch
After polyfill+node-fetch: 99.99% compatible ✅
Please Tweet at me if you have other Use Cases you’d like to see. ❤️