axios began as a vanilla JavaScript answer to AngularJS's $http service, sketched out by Matt Zabriskie in a hotel room at MidwestJS. Working directly with XMLHttpRequest was unpleasant, and a library that ran in both NodeJS and the browser was a genuine step forward. Nearly six years on, it still sees an enormous and growing number of npm downloads, and while Matt has moved on, the project remains actively maintained.
What changed in the meantime is the platform itself. The browser now ships fetch, a promise-based HTTP API with a far better developer experience. It is widely supported and easy to polyfill — unfetch is one option. That makes a self-written wrapper a realistic replacement for axios in browser code, for a few reasons:
- Less API to learn
- Smaller bundle size
- Reduced trouble when updating packages and managing breaking changes
- Immediate bug fixes and releases
- Conceptually simpler
A wrapper from the bookshelf app is a good working example. It starts as a single client function:
function client(endpoint, customConfig) {
const config = {
method: 'GET',
...customConfig,
}
return window
.fetch(`${process.env.REACT_APP_API_URL}/${endpoint}`, config)
.then((response) => response.json())
}
Called from application code, it looks like this:
client(`books?query=${encodeURIComponent(query)}`).then(
(data) => {
console.log('here are the books', data.books)
},
(error) => {
console.error('oh no, an error happened', error)
},
)
Error handling
The built-in window.fetch does not behave like axios when a request comes back with a client error. Its promise rejects only when the request itself fails, i.e. on a network error. The Response object exposes an ok property, which the wrapper can check in order to reject on non-OK responses too:
function client(endpoint, customConfig = {}) {
const config = {
method: 'GET',
...customConfig,
}
return window
.fetch(`${process.env.REACT_APP_API_URL}/${endpoint}`, config)
.then(async (response) => {
if (response.ok) {
return await response.json()
} else {
const errorMessage = await response.text()
return Promise.reject(new Error(errorMessage))
}
})
}
With that in place, the promise chain rejects whenever the response is not ok.
Sending data
Posting a body is already possible through the current API, but it can be made more convenient:
function client(endpoint, { body, ...customConfig } = {}) {
const headers = { 'Content-Type': 'application/json' }
const config = {
method: body ? 'POST' : 'GET',
...customConfig,
headers: {
...headers,
...customConfig.headers,
},
}
if (body) {
config.body = JSON.stringify(body)
}
return window
.fetch(`${process.env.REACT_APP_API_URL}/${endpoint}`, config)
.then(async (response) => {
if (response.ok) {
return await response.json()
} else {
const errorMessage = await response.text()
return Promise.reject(new Error(errorMessage))
}
})
}
Which allows calls such as:
client('login', { body: { username, password } }).then(
(data) => {
console.log('here the logged in user data', data)
},
(error) => {
console.error('oh no, login failed', error)
},
)
Authentication and 401s
Authenticated requests can be handled in more than one way. The bookshelf app's approach reads a token from localStorage and, if present, attaches an Authorization header following the JWT spec, so the server can decide whether the user is authorized — a very common practice.
const localStorageKey = '__bookshelf_token__'
function client(endpoint, { body, ...customConfig } = {}) {
const token = window.localStorage.getItem(localStorageKey)
const headers = { 'Content-Type': 'application/json' }
if (token) {
headers.Authorization = `Bearer ${token}`
}
const config = {
method: body ? 'POST' : 'GET',
...customConfig,
headers: {
...headers,
...customConfig.headers,
},
}
if (body) {
config.body = JSON.stringify(body)
}
return window
.fetch(`${process.env.REACT_APP_API_URL}/${endpoint}`, config)
.then(async (response) => {
if (response.ok) {
return await response.json()
} else {
const errorMessage = await response.text()
return Promise.reject(new Error(errorMessage))
}
})
}
A companion trick covers expired or otherwise invalid tokens: when response.status is 401, log the user out automatically and refresh the page for them.
const localStorageKey = '__bookshelf_token__'
function client(endpoint, { body, ...customConfig } = {}) {
const token = window.localStorage.getItem(localStorageKey)
const headers = { 'content-type': 'application/json' }
if (token) {
headers.Authorization = `Bearer ${token}`
}
const config = {
method: body ? 'POST' : 'GET',
...customConfig,
headers: {
...headers,
...customConfig.headers,
},
}
if (body) {
config.body = JSON.stringify(body)
}
return window
.fetch(`${process.env.REACT_APP_API_URL}/${endpoint}`, config)
.then(async (response) => {
if (response.status === 401) {
logout()
window.location.assign(window.location)
return
}
if (response.ok) {
return await response.json()
} else {
const errorMessage = await response.text()
return Promise.reject(new Error(errorMessage))
}
})
}
function logout() {
window.localStorage.removeItem(localStorageKey)
}
Depending on the situation, re-routing to a login screen instead of refreshing may fit better.
Beyond the base client, applications tend to grow additional wrappers for specific resources, such as list-items-client.js:
import { client } from './api-client'
function create(listItemData) {
return client('list-items', { body: listItemData })
}
function read() {
return client('list-items')
}
function update(listItemId, updates) {
return client(`list-items/${listItemId}`, {
method: 'PUT',
body: updates,
})
}
function remove(listItemId) {
return client(`list-items/${listItemId}`, { method: 'DELETE' })
}
export { create, read, remove, update }
When to keep axios
axios does a lot on your behalf, and if it is working well there is no need to abandon it — it remains a good fit for Node projects. For the browser, though, a small wrapper around fetch that does exactly what the application needs and nothing more is often the better trade. Anything an axios interceptor or transform can do, the wrapper can do; and if some behavior should not apply to every request, wrap the wrapper.



