Ever since first being introduced, Fetch API has sort of become the de facto standard for fetching resources and interfacing with Backend API for modern web applications.
While similar to XMLHttpRequest, fetch provides a way more powerful API with a more flexible feature set. It is also available in window
as well as worker
and there are also libraries like node-fetch that allow it to be used in nodejs, basically fetch is available almost anywhere and in any context.
It’s promise based API makes it really simple to load resources asynchronously and also makes it simple to handle more complex cases like conditionally chaining fetching of other resources etc.
While fetch() is great and really solves almost all the hassles of making API calls, often when using it (or actually any other method like XMLHttpRequest, or axios etc), we end up having to handle a lot of cases, from different error codes, to cases where network request fails, parsing of the response body into json or text, extracting or deciphering error reason to show to the user or to log etc.
This often results in massive blocks being repeated with every Backend API interfacing function. The following code snippet would look very familiar to a lot of Frontend web developers:
fetch(`${API_BASE_URL}/api/v1/categories`)
.then((response) => {
if ((response.status === 200) || (response.status === 400) || (response.status === 401)) {
return response.json();
}
})
.then((json) => {
if (!Object.keys(json).includes('errors')) {
// handle json.data
} else if (json.errors[0] === 'Invalid token.') { // in case of error, API returns array of error messages
// handle error due to invalid token, initiate re-login or something else
} else {
// handle any other error status codes
}
})
.catch(() => {
// handle any other case, like json parse failure or network error
});
Obviously, there is a lot going wrong in the above function, but can we make this better ?
In case of any Backend API method, there would be status codes that indicate success cases (200, 201 etc) and standard ways to denote errors in case of failing status codes like (401, 404, 500 etc).
The above code can be greatly simplified and made way less brittle if we can standardise the interface of our Backend API and use that standardised interface to make API calls.
With that in mind, we can creating sort of a wrapper function that wraps our Backend API calls using fetch() and provides us with a standard interface to our Backend API call results, whether successful or failing.
I have been using a function along these lines in a lot of my Frontend codebases and it has really helped simplify Backend API calls and to rapidly add new methods.
const responseParserTypes = {
json: (response) => response.json(),
text: (response) => response.text(),
blob: (response) => response.blob(),
formData: (response) => response.formData(),
arrayBuffer: (response) => response.arrayBuffer(),
};
const parseResponse = (response, type) => {
if (!Object.keys(responseParserTypes).includes(type)) {
return null;
}
return responseParserTypes[type](response);
};
const fetchHandler = (
fetchPromise,
{
handledStatusCodes = [200],
parseHandledResponseAs = 'json',
parseUnhandledResponseAs = 'text',
getUnhandledResponseMessage = () => 'Error occured',
getFailureMessage = () => 'Error occured',
},
) => {
if (!Object.keys(responseParserTypes).includes(parseHandledResponseAs)) {
throw new Error(`parseHandledResponseAs shouwld be one of [${Object.keys(responseParserTypes).join(', ')}]`);
}
if (!Object.keys(responseParserTypes).includes(parseUnhandledResponseAs)) {
throw new Error(`parseUnhandledResponseAs shouwld be one of [${Object.keys(responseParserTypes).join(', ')}]`);
}
return new Promise((resolve, reject) => {
fetchPromise
.then((response) => {
if (handledStatusCodes.includes(response.status)) {
const parseResponsePromise = parseResponse(response, parseHandledResponseAs);
parseResponsePromise
.then((parsedResponse) => resolve(parsedResponse))
.catch((e) => reject(getFailureMessage(e)));
} else {
const parseResponsePromise = parseResponse(response, parseUnhandledResponseAs);
parseResponsePromise
.then((parsedResponse) => reject(getUnhandledResponseMessage(
response.status,
parsedResponse,
)))
.catch((e) => reject(getFailureMessage(e)));
}
})
.catch((e) => reject(getFailureMessage(e)));
});
};
export default fetchHandler;
You can also find this at https://gist.github.com/sidevesh/adaf910bc384574b776c370f77b9bedf , this may also be more updated in future.
Now let’s see how the same callCategoriesIndexPageItemsLoad
method can be simplified using the above fetchHandler
function.
export const getCategories = fetchHandler(
fetch(`${API_BASE_URL}/api/v1/categories`),
{
handledStatusCodes = [200],
parseHandledResponseAs = 'json',
parseUnhandledResponseAs = 'json',
getUnhandledResponseMessage = (statusCode, parsedResponseBody) => {
if (statusCode === 401) {
return 'Looks like you are logged out, redirecting to log in page...';
} else if (statusCode === 500) {
return 'Something went wrong, we are looking into it';
} else {
return 'Unknown error';
}
},
getFailureMessage = (e) => {
// return proper error message for other failures,
// like json parse error or network failure,
// that can be figured out using the exception argument provided
return 'Network error occured';
},
},
)
With the above implementation, we would get proper error messages for each error status code as well as any other exception that can be then shown in the UI.
Also, this automatically handles parsing the response so no need to chain the response.json()
, reponse.text()
or any other response parsing call.
To use this method to get data is as simple as:
getCategories()
.then((json) => {
// handle json.data
})
.catch((errorMessage) => {
// show errorMessage
});
This should cover a lot of use cases, if there is need to perform actions in case of failure then rather than returning string
message in getUnhandledResponseMessage
and getFailureMessage
, we may also return an object containing the message as string
and statusCode
or something else, which can then be checked for, and corresponding action can be performed.
That’s it then, hope this helps you streamline your Backend API call methods.
Let me know if you have a better way or know of a library that helps with same.
Hope this was helpful, if it was then do follow me on twitter for more such articles.
Cheers!