DEV Community

Discussion on: You Don't Need Axios

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.

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