Why I won’t be using Fetch API in my apps

To fetch or not to fetch?

When Fetch API became standard I was thrilled. I will no longer need to use http utility libraries in order to make http calls in my apps. XMLHttpRequest was so low level and awkward (up to its inconsistent camel casing of acronyms!). You didn’t really have a choice but to wrap it with something more comfortable or choose one of tons of open source alternatives like jQuery’s $.ajax(), Angular’s $http, superagent and my personal favorite, axios. But are we truly free of http toolkits?


With fetch, I thought, I don’t need to choose between this boatload of utility libraries. I no longer need to debate with my colleagues over which one is best. Instead I can just bring in a fetch polyfill when needed and use an API which is standard, which was designed with modern use cases and lessons learned in mind.

Sadly, when looking into some pretty basic real life use cases, we see that http toolkits are here to stay. While fetch is obviously a welcome addition which will help us with doing low level operations easily, that’s what it is. It is a low level API which in most applications should not be used directly, but with more suitable abstractions.

Error handling

When you look at fetch basic examples, it looks very appealing and very similar to other http utilities we are used to. Let’s see a trivial example. Instead of doing:

axios.get(url)
.then(result => console.log('success:', result))
.catch(error => console.log('error:', error));

You would do:

fetch(url).then(response => response.json())
.then(result => console.log('success:', result))
.catch(error => console.log('error:', error));

That’s really easy, right? We needed to add that response.json() in there, but that’s a small price to pay if we get support for response streams. In my mind, needing response streams is an edge case and usually I don’t let edge cases effect the common case. I’d probably preferred to allow users to pass some flag if they want a stream instead of giving everyone a stream. But really, that’s not such a big deal.

What is a big deal, and what I’m sure many readers missed in the above example (as did I, when I first used fetch), is that actually the two code snippets above do not do the same thing. All of the http toolkits I mentioned above (seriously, each and every one of them) treat a response with error status from the server (i.e. status 404, 500, etc.) as an error. However, fetch (similarly to XMLHttpRequest) rejects the promise only in case of network error (i.e. address could not be resolved, server is unreachable or CORS not permitted).

This means that when server returns 404, we will print ‘success’ to the console. If we want to do the more intuitive thing for an application developer and return a promise rejection in case server responded with an error, we’ll need to do something like this:

fetch(url)
.then(response => {
return response.json().then(data => {
if (response.ok) {
return data;
} else {
return Promise.reject({status: response.status, data});
}
});
})
.then(result => console.log('success:', result))
.catch(error => console.log('error:', error));

I’m sure many people might say: “What’s the problem here? you requested data from the server and you got it. So what if the server responded with status 404, it is a response from the server all the same”. And they will be right. It is only a matter of perspective. For an application developer, in my opinion, an error response from the server is almost always considered an exception and should be treated the same as network failure. In order to fix that behavior, we can’t just change the standard behavior of fetch. We simply need a better abstraction which is suitable for application developers.

POST requests to server

Another very common use case for an application developer is sending POST request to server. With an http toolkit such as axios, you’d do something like this:

axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
});

When I first started using Fetch API, I was really optimistic. I thought: boy, this brand new API is so similar to what I’m used to. Sadly I ended up wasting almost a full hour on sending a POST request to my server because this did not work:

fetch('/user', {
method: 'POST',
body: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});

What I needed to learn the hard way, as did many other developers, I’m sure, is that fetch, being the low level API that it is, doesn’t give you shortcuts for solving common use cases such as this. The Fetch API is very explicit. The JSON must be converted to a string and the ‘Content-Type’ header must indicate that the payload is JSON, otherwise the server will treat it as a string. What we actually should have done is:

fetch('/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
firstName: 'Fred',
lastName: 'Flintstone'
})
});

Well, this starts to be too much for me to repeat in every API call I write. And there’s more!

Defaults

As you can see so far, fetch is a very explicit API, you don’t get anything unless you ask for it. So for example, none of the the fetch calls described above would actually work on my server, because:

  1. My server uses cookie based authentication and fetch does not send cookies by default.
  2. My server needs to know that the client will be able to handle a JSON encoded response.
  3. My server is on a different sub-domain and CORS is disabled by default in fetch.
  4. In order to block XSRF attacks, my server requires every API call to be accompanied by a custom X-XSRF-TOKEN header which proves that the API call is being made from my application page.

So what I should actually be doing is:

fetch(url, {
credentials: 'include',
mode: 'cors',
headers: {
'Accept': 'application/json',
'X-XSRF-TOKEN': getCookieValue('XSRF-TOKEN')
}
});

It is totally fine that fetch does not do all those things by default, but if I’m going to use fetch all over my application for making API calls, I need a way to modify those defaults to what makes sense in my application. Sadly, fetch does not give me any mechanism to override defaults. You already guessed by yourself who does:

axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Accept'] = 'application/json';
axios.defaults.headers.post['Content-Type'] = 'application/json';

But that’s just for show, because actually all of the things mentioned above, including XSRF protection is something axios gives me by default already. The purpose of axios is to give a an easy to use tool for making API calls to your server. The purpose of fetch is much wider than what I’m using it for, that’s why it is not the best tool for the job.

Conclusion

Saying that you don’t need an http toolkit, means that instead of a single line:

function addUser(details) {
return axios.post('https://api.example.com/user', details);
}

What you are actually going to do is this:

function addUser(details) {
return fetch('https://api.example.com/user', {
mode: 'cors',
method: 'POST',
credentials: 'include',
body: JSON.stringify(details),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-XSRF-TOKEN': getCookieValue('XSRF-TOKEN')
}
}).then(response => {
return response.json().then(data => {
if (response.ok) {
return data;
} else {
return Promise.reject({status: response.status, data});
}
});
});
}

This is obviously not something you would repeat in every API call you do. You’d probably extract it to a function and ask people who work on your project to use that function instead of fetch.

Then when the next project comes along you will take that function and extract it to a library. Then when more requirements come, you’ll simplify the API, you’ll make it more customizable, fix all of the bugs and find ways to make your API consistent. Then you’ll even throw in a couple of features like request cancelation, progress and custom timeouts.

You might even do a pretty good job. But all you did is created another http toolkit and eventually you used it in your projects instead of using the hot new Fetch API after all. It’s probably better you save yourself the time and effort and npm install --save axios (or a similar toolkit of your choosing).

And really, do you think it matters if this http toolkit uses fetch internally or XMLHttpRequest?

P.S.

I just want to stress again: this is not a rant against fetch! I don’t think that the points presented are design flaws, all of them make perfect sense for a low level API. I’m just saying that I wouldn’t recommend to use low level API’s such as this directly in your application. I simply think people should use tools that abstract the low level layer and give them more high level API which better suits their purpose.