DEV Community

Olufisayo Bamidele
Olufisayo Bamidele

Posted on

You Don't Need Axios

You don't need Axios, well, for most use cases. The web is maturing, and the days of Axios are ending.

Why we needed Axios

For newcomers, not long ago, we needed to use XMLHttpRequest to fetch data from servers. To fetch data from https://jsonplaceholder.typicode.com/todos/, we would have needed the following lines of code at the very minimum.

const req = new XMLHttpRequest();
req.addEventListener("load", function() {
  console.log(JSON.parse(this.responseText))
})

req.open('GET', 'https://jsonplaceholder.typicode.com/todos/')
req.send()
Enter fullscreen mode Exit fullscreen mode

We needed an elegant abstraction over XMLHttpRequest, and axios filled that space perfectly, especially with the introduction of Promise in ES6.

With axios, we can achieve the same thing with just one line of code

    const data = (await axios.get('https://jsonplaceholder.typicode.com/todos/')).data
Enter fullscreen mode Exit fullscreen mode

But time has passed, the web has matured, and now, we have fetch()

Use fetch() Instead

With fetch, you get most of the benefits of Axios with zero external dependencies in your application. I am a proponent of minimal dependencies. If the platform I'm on provides a functionality, and it's relatively easy to implement, I'll explore it first. This is even more important with JavaScript runtimes, as every abstraction comes at a cost.

GET Request Using Fetch

    const data = await fetch('https://jsonplaceholder.typicode.com/todos/')).then(res => res.json())
Enter fullscreen mode Exit fullscreen mode

POST Request Using Fetch

The fetch API takes in a second parameter, which allows you to specify other options, including your HTTP method, headers, and body. The example below shows how to make a post request using fetch API. This example also applies to other HTTP methods that mutate the server;

    const data = await  fetch('https://jsonplaceholder.typicode.com/todos/', { 
  method: 'POST', 
  body: JSON.stringify({title: 'buy milk'})
}).then(res => res.json())
Enter fullscreen mode Exit fullscreen mode

Uploading Files

Assuming you have the following HTML file

<!Docktype html>
<html lang=en>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width initial-scale=1.0">
        <meta http-eqiv="X-UA-Compatibly" content="ie=edge">
        <title>File Upload</title>
    </head>
    <body>
        <form id="fileform" enctype="multipart/form-data">
            <input name="upload"  type="file" placeholder="upload file" />
            <button type="submit">submit</button>
        </form>
        <script src="./index.js"></script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

then your fetch call would look like this:

const form = document.getElementById("fileform")

form.addEventListener("submit", (event) => {
    event.preventDefault();
    const fileInput = document.getElementsByName('upload')[0]

    const fd = new FormData()
    fd.append('upload', fileInput.files[0]);

    fetch('http://localhost:5001/upload', { method: 'POST', body: fd}).then(res => res.json().then(body => console.log('result', body)))
})
Enter fullscreen mode Exit fullscreen mode

Pro Tip

Always wrap your fetch function in with another function; This gives you leeway for abstracting HTTP call patterns emerging as your application grows. See the code snippet below.

 const originalFetch = window.fetch
window.fetch = async (...args) => {
    // intercept request
    const res = originalFetch(...args)
    // intercept response e.g
    if(res.ok) {
        return res.json()
    } else
        //Do whatever you want to handle the error
    }
}
Enter fullscreen mode Exit fullscreen mode

The Whataboutisms

What about Request and Response Interceptors

We've talked about that briefly in the previous subheading

    const interceptRequest = (...args) => {
        // perform some magic
        return args
    }

    const interceptResponse = (res) => {
        // perform some magic
        return res;
    }

 const originalFetch = window.fetch
window.fetch = async (...args) => {
    const modifiedArgs = interceptRequest(...args)
    const res = await orginalFetch(modifiedArgs)
    return interceptResponse(res)
}
Enter fullscreen mode Exit fullscreen mode

Edit 24 Oct 2023 More On Intercepting Requests

Due to questions from colleagues, I realized I needed to address this a little more.

To intercept means to stop something from proceeding. Interceptors in HTTP libraries(you could call them middleware), stop requests and modify the request configuration using your provided function. For response, interceptors take raw server responses and modify them based on your logic before returning them to the library consumers.

These can be implemented as simple functions that get called before and after your call to fetch

Axios and other JS libraries don't do magic with your interceptors. They only call the functions you give to them and call them for you

What About Timeouts

Timeouts should primarily be the business of your backend. It would help if you also had a backend that integrates the third-party platforms, but should you choose to incorporate an unreliable backend with your frontend, you can still implement timeouts with fetch API so

// timeout request after 5 seconds
const res = await fetch('https://jsonplaceholder.typicode.com/todos/', { abort: AbortSignal.timeout(5000) })
Enter fullscreen mode Exit fullscreen mode

What About Javascript In The Backend, Aka Nodejs/Deno/Bun

The three significant runtimes now implement fetch, and if you must install any HTTP client for your javascript backend, install undici. Undici is an HTTP client built from the ground for nodejs, and the nodejs organization maintains it. Undici also implements fetch API

What about a progress update for large file uploads

Use XMLHttpRequest ;). Unfortunately, fetch() has yet to include this feature.

Conclusion

Regarding dependencies, I follow the YAGNI principle in extreme programming. There is no need to bloat your application with thousands of dependencies. The web is maturing, and like JQuery, axios' time is gradually ending. With current developments, Axios and other popular HTTP client libraries are no longer required to have good developer experience.

Top comments (35)

Collapse
 
adaptive-shield-matrix profile image
Adaptive Shield Matrix

You may not need axios, just like you do not need any luxuries in life.

But why would you avoid axios?
Axios provides a much better api / dev experience calling http endpoints. That's a plus.
What are the downsides, except bundle size ?

axios bundle size is 57 kB -> 21.8 kB (gzip)
bundlejs.com/?q=axios%401.5.1

Collapse
 
ngfizzy profile image
Olufisayo Bamidele

Hi @adaptiveshieldmatrix

Thanks for your feedback. I agree with you on the DX part.

I know the article says "axios" but it's really about not bringing in dependencies unless the DX is so poor you'd throw your laptop out if you don't bring in a library.

Picking on axios one more time: axios have three dependencies, one of which is form-data. We don't need this dependency in the browser because the browser natively supports form data, and you also don't need it in nodejs from node 18+. Form data also have 3 other dependencies that also have their dependencies. The dependency tree goes on and on, and at any point, one of the maintainers might decide to hit the kill switch on the project. For example, form-data's last update was three years ago, but it currently has 113 open issues.

All that dependency when what most(not all) web applications out there do is fetch data and map to JSX or render to HTML, listen to events, submit data, update state, and repeat.

Collapse
 
adaptive-shield-matrix profile image
Adaptive Shield Matrix

1.
I think its okay for library to not get any updates if the protocol (multipart, like in the case of form-data) is stable.
Do all libraries have to release new versions just for appearance sake?

Axios has many issues listed
github.com/axios/axios/issues?q=is...
but no bugs (mostly feature requests and questions about edge cases)
and seems very safe / stable to use.

I can understand your strife to reduce dependencies, but that are the alternatives ? Write http data fetching libraries yourself
or suffer poor DX because of raw XMLHttpRequest / fetch.

2.
I have looked a bit more at http client libraries

Here seems to be a good comparison
github.com/sindresorhus/got#compar...

Following your main grip against axios -> ky seems to be the next best choice for http clients in the browser
github.com/sindresorhus/ky

With a bundle size of only 9.12 kB -> 3.23 kB gzip
https://bundlejs.com/?q=ky%401.1.0&treeshake=%5B*%5D

Thread Thread
 
ngfizzy profile image
Olufisayo Bamidele

thanks. Would check the links out

Collapse
 
joshuaamaju profile image
Joshua Amaju

One reason to avoid axios, difficult to handle errors.

Collapse
 
adaptive-shield-matrix profile image
Adaptive Shield Matrix

How is it more difficult to handle than fetch?

You can even overwrite/define then it throws errors
pipedream.com/community/t/faq-how-...

Thread Thread
 
joshuaamaju profile image
Joshua Amaju

it's not about when it throws, it's how it handles errors. It hides the response in error.response.data, which becomes a big issue when dealing with typescript.

Thread Thread
 
adaptive-shield-matrix profile image
Adaptive Shield Matrix

How is accessing the response with error.response.data in typescript a big problem?

Thread Thread
 
joshuaamaju profile image
Joshua Amaju • Edited

When using asyn/await you loose all types, so you have to do

if (isAxiosError(error)) {
  let err = error.response?.data?.whatever
}
Enter fullscreen mode Exit fullscreen mode

And I just don't like writing error.response?.data?.whatever in general. I generally prefer dealing with Response.

And there's no way to conditionally tell axios to not parse responses. One big reason to not use axios is because of the all or nothing behaviour. You have build a client for each scenario (which is unrealistic)

Thread Thread
 
adaptive-shield-matrix profile image
Adaptive Shield Matrix

You can type axios, example:
await axios.post<EventType[] | { errorMessage: string }>(urlApiCmdPublic, cmd)
so I would not count that as a downside. Fetch in comparison does not have any types at all.

"no way to conditionally tell axios to not parse responses" - that's a fair point,
but its relevant only if using some non-http/rest APIs - which to be fair should be pretty rare.

I agree with you, that axios is mostly designed to consume http/rest apis with good developer experience
and having the niche use-case of calling lower level binary apis is best served using lower level tools like fetch.

Thread Thread
 
joshuaamaju profile image
Joshua Amaju • Edited

await axios.post<EventType[] | { errorMessage: string }>(urlApiCmdPublic, cmd) only works for non-error responses.

Fetch in comparison does not have any types at all

Not exactly true, you get aResponse which makes sense. And then you can convert to json and do validation i.e using zod schema.parse(await response.json()) to get a concrete type. Which is a much better approach than what axios offers

Collapse
 
webjose profile image
Josรฉ Pablo Ramรญrez Vargas

Axios provides a much better api / dev experience

I would be very interested in seeing exactly what you refer to. What am I gaining, exactly, for the extra 57kb?

Collapse
 
adaptive-shield-matrix profile image
Adaptive Shield Matrix

The top things I can think of:

  • there are many different ready-made adapter for axios, including rate limiting
  • you can provide defaults, base paths while instantiating axios
  • not having to async call json() (in comparison to fetch) to get the data
  • having shorthand methods for get/post/...

With the release of fetch this list got much shorter...
I think I'm slowly coming around to using fetch on the browser as well

Thread Thread
 
webjose profile image
Josรฉ Pablo Ramรญrez Vargas

I see. I think the author (and I agree) is on the line of "90% of people don't need adapters, don't need base paths or have them covered elsewhere". I think that for 9 out of 10 developers, axios is 57kb of "nothing I need".

I imagine that the adapters are probably very handy, but are also probably very specific and catered for the minority, not the majority.

So yes, I think you are doing the right thing considering fetch() as the primary tool here. If you need interception of request or response, that's something that can be easily done in far less than 57kb.

Cheers!

Collapse
 
digitalbrainjs profile image
Dmitriy Mozgovoy

axios bundle size is 57 kB -> 21.8 kB (gzip)

Its size is 13.7 kB gzipped (ESM)
18.8 kB gzipped (UMD, ES5)

Collapse
 
teamradhq profile image
teamradhq

Never Mutate Global State

I suggest that mutating global state like this is the opposite of a pro tip:

const originalFetch = window.fetch
window.fetch = async (...args) => {
    // intercept request
    const res = originalFetch(...args)
    // intercept response e.g
    if(res.ok) {
        return res.json()
    } else
        //Do whatever you want to handle the error
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, every single client side request that uses fetch under the hood will not work as expected. You've literally broken every request.

In a commercial setting, you're almost guaranteed to be running some vendor scripts on the client. Things like logging services, tracking and marketing, A|B tests, heat mapping or service integrations. All of these will no longer work as expected if they use the fetch API. Not to mention any browser extensions or user scripts that may be loaded on the client...

Even the simple action of transforming the response to JSON like this will break any call to fetch.

Broken Implementation

Just try this example after mutating global.fetch:

async function example() {
  try {
    const textRes = await fetch('https://google.com');
    console.log(await textRes.text());
  } catch (error) {
    console.log('Text Error');
    console.log(error);
  }

  try {
    const jsonRes = await fetch('https://api.publicapis.org/entries');
    console.log(await jsonRes.json());
  } catch (error) {
    console.log('JSON Error');
    console.log(error);
  }
}
Enter fullscreen mode Exit fullscreen mode

Calling this function will output:

Text Error
SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON at JSON.parse (<anonymous>)

JSON Error
TypeError: jsonRes.json is not a function
Enter fullscreen mode Exit fullscreen mode

This is incredibly difficult to reason about as these errors are misleading.

In this case, vendors who use axios are better off because this mutation doesn't affect XMLHttpRequest.

Use Encapsulation

This is closer to what I would consider a professional approach:

// client.ts
export async function client(
  input: RequestInfo | URL,
  init?: RequestInit
): Promise<Response> {
  const res = await fetch(input, init);

  // Do whatever you want with res

  return res;
}

// someFunction.ts
import { client } from './client';

export async function someFunction() {
  return await client('https://example.com');
}
Enter fullscreen mode Exit fullscreen mode

Neither Choice is Superior

The choice between axios and fetch depends on the context of the problem. You've laid out a number of examples that, from your perspective, make axios seem redundant to you. However, your examples actually make a strong case for using axios in a commercial setting to me.

As someone who's created some XMLHttpRequest implementations by hand, I have similar misgivings to using fetch today that I did back then.

It really highlights the convenience that axios provides. I look at your form examples and I see a lot of additional code that the team has to maintain.

Your example has zero dependencies:

form.addEventListener("submit", (event) => {
    event.preventDefault();

    const fd = new FormData()
    fd.append('upload', fileInput.files[0]);

    fetch('http://localhost:5001/upload', { method: 'POST', body: fd})
      .then(res => res.json()
        .then(body => console.log('result', body))
      )
})
Enter fullscreen mode Exit fullscreen mode

This example is less complex, more convenient and less maintenance:

form.addEventListener("submit", (event) => {
    event.preventDefault();
    axios.postForm('http://localhost:5001/upload', { upload: fileInput.files[0] })
      .then(res => console.log('result', res.data))
})
Enter fullscreen mode Exit fullscreen mode

Reinventing the Wheel vs Getting Things Done

I want to reiterate that I don't see any choice as superior to the other before sharing my experience using fetch out of the box in a project.

It almost always ends up with something like this to make a basic request:

const base64Credentials = btoa(username + ':' + password);
const headers = new Headers({
    'Authorization': 'Basic ' + base64Credentials,
    'Content-Type': 'application/json',
    'Custom-Header': 'CustomHeaderValue'
});

fetch('https://example.com/api/resource-a', {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(requestBody)
}).then(response => {
    if (!response.ok) {
        throw new Error('Network response was not ok');
    }
    return response.json();
}).then(data => {
    console.log(data);
}).catch(error => {
    console.log('There was a problem with the fetch operation:', error.message);
});
Enter fullscreen mode Exit fullscreen mode

Then a stakeholder comes along and adds a new business requirement to fetch something from resource-b as well. So the fetch logic is encapsulated:

export function fetchFromApi(path, jsonBody) {
    const baseUrl = 'https://example.com/api/';
    const url = baseUrl + path;

    // Basic Authorization header
    const base64Credentials = btoa(username + ':' + password);
    const headers = new Headers({
        'Authorization': 'Basic ' + base64Credentials,
        'Content-Type': 'application/json',
        'Custom-Header': 'CustomHeaderValue'
    });

    return fetch(url, {
        method: 'POST',
        headers: headers,
        body: JSON.stringify(jsonBody)
    })
    .then(response => {
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        return response.json();
    });
}

fetchFromApi('/resource-a', aBody)
    .then(handleABody)
    .catch(handleError);

fetchFromApi('/resource-b', bBody)
    .then(handleBBody)
    .catch(handleError);
Enter fullscreen mode Exit fullscreen mode

This is the reality that you face when building complex applications with fetch. It's a considerable investment to save 23KB on a request.

To achieve the same result with axios:

export const client = axios.create({
  baseURL: 'https://example.com/api/',
  auth: {
    username,
    password,
  },
  headers: {
    'Custom-Header': 'CustomHeaderValue'
  }
});
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ngfizzy profile image
Olufisayo Bamidele

On Not Mutating Global State

That is an excellent point on not mutating the global state. Thanks for the callout. The main point I was trying to make is to always encapsulate(wrap).

In my codebase, it looks something like this.

const xwzClient = (...args) => {
     // prepare request
      // do fetch
     // inspect response
    // inspect response
    // return response
}
Enter fullscreen mode Exit fullscreen mode

I'll do an edit sometime during the day to reflect this.

To reinvent the wheel or not to
This topic is very subjective, so I'll have to answer subjectively. Having written and made HTTP requests in another language (i.e. go and rust) in the past year, returning to fetch didn't seem like a lot of work.

Arguably, many of the JS libraries out there are reinventing the wheel for better or worse.

At the end of the day, it's up to your team to decide how much re-invention is too much reinvention.

On whether one is superior to the other
This is not a subjective one, so here are my plus for fetch

  • It's in the browser forever, so your code is insured for life
  • Whatever abstraction you choose to build on it belongs to your team. Fortunately, from most codebases I've worked on, your HTTP client root config is usually the least touched module as your codebase grows.
  • It's faster: Checking some random benchmarks on the internet, fetch always comes out at least 2x faster than axios. This is not important to me because as your team builds its abstraction over fetch, your requests might get slower.
Collapse
 
barrymichaeldoyle profile image
Barry Michael Doyle

I used to have this opinion that axios wasn't worth using over fetch. But lately I've found that it has some nice middleware features that fetch doesn't have. So I'd say it isn't worth throwing out just yet.

I guess it all just depends on how you use it in the end. If you're not using some of those special axios features then by all means don't add it as a dependency to your project when you can just use fetch.

Collapse
 
ngfizzy profile image
Olufisayo Bamidele

I completely agree with you that some large-scale projects especially the ones that require heavy file uploads will still benefit much from axios.

Earlier this year, I took over a project and I was kindof irritated by the fact that the original developer used just fetch. I thought it would end up being a problem for me along the line but after many months, I completely stopped missing axios. This is what inspired this article.

Collapse
 
barrymichaeldoyle profile image
Barry Michael Doyle

Ok yeah that makes a lot of sense! Thanks for sharing :)

Collapse
 
rickdelpo1 profile image
Rick Delpo

Hey thanks, nice and concise explanation. I am a proponent of ditching all JS Libraries for most use cases. Without JQuery or Axios we can now do everything in Plain Vanilla JS. Fetch is definately the way to go. Also I ditched my databases too and am now all serverless using json for my small use case and non relational data sets. PS, I am not a student so can get away without using React or Node. I use only small apps for everything, like AWS Lambdas for example. Everything as a Service !!

Collapse
 
adaptive-shield-matrix profile image
Adaptive Shield Matrix

I followed your recommendation and took a look at undici

There is a long discussion about fetch performance
github.com/nodejs/undici/issues/1203

For example in this benchmark (updated about a month ago)
github.com/silverwind/fetch-bench
undici: 2966ms
axios: 2429ms
-> axios is faster than undici

The increased bundle size of axios (57 kB -> 21.8 kB gzip) is not an issue on the server (only in the client/browser)

I would say, that it is a contested point, which of the 2 libraries is better axios or undici

Collapse
 
ngfizzy profile image
Olufisayo Bamidele

Interesting. I'm watching that space now to see how it evoves overtime. Thanks for sharing. We learn everyday.

Collapse
 
ingosteinke profile image
Ingo Steinke, web developer

Timeouts are not only backend related. We could have a connection timeout without even reaching the backend, or a receive timeout when the network connection drops.

Collapse
 
bitknight profile image
BitKnight

Thanks but Axios saved one of my projects. However you bring up a valid point with Fetch giving you the option to not have any dependencies in your project. I had issues with Fetch before but I will give it a crack again sometime.

Collapse
 
ngfizzy profile image
Olufisayo Bamidele

Thanks Matthew. Contrary to what it might sound like, I'm not advocating against using axios. I'm just raising awareness that we now have this cool thing called fetch and it's getting really good. ๐Ÿ˜Š

Collapse
 
bias profile image
Tobias Nickel

Yes, i also often think it is much easier to wrap the fetch api than learning the api of axios.

Collapse
 
shinigami92 profile image
Shinigami

The AbortSignal Timeout was new to me ๐Ÿ‘Œ

But Axios is more then getting data, itโ€™s also well structured Error handling and developer experience (DX)

Collapse
 
moxie_deezee profile image
Adisat Eletu

Iโ€™m a big fan of axios, but I am not offended by this ๐Ÿ˜† very nice write up. ๐Ÿ™Œ๐Ÿป

Some comments have been hidden by the post's author - find out more