I have finally made the decision to let go of one of my favorite NPM packages, Axios, in favor of modern browsers' Fetch API. I want to be clear up front - I find nothing wrong with Axios, it is an extremely high quality package and a natural progression having used Angular’s http service that it was originally based upon. I will likely still rely on Axios in NodeJS projects, but times change and it now seems a bit redundant in front-end client applications.
There are really only a few factors that are prompting me to make this change:
- There is no longer a need for me to support legacy browsers
- A need to get back exactly what was being sent by the server without any additional overhead
- No longer seeing the necessity of such a package on the client when the native browser API is more than sufficient
Before#
So here is a typical, albeit contrived, previous use of Axios in a front-end service on my Redux template project :
file: /src/services/user/UsersApi.ts
import { AxiosResponse } from 'axios';
import IUser from '../../@interfaces/IUser';
import { getAxiosInstance } from '../baseService';
import { unwrapServiceError } from '../../util/service';
axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com/';
export const fetchUsers = async (): Promise<Array<IUser>> => {
try {
const axios = getAxiosInstance();
const response: AxiosResponse = await axios.get('/users');
return response.data;
} catch (e) {
throw unwrapServiceError('UsersApi.fetchUsers', e);
}
};
Along with the factory method to create the Axios instance:
file: /src/services/baseService.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { config as appConfig } from '../config';
export const getAxiosInstance = (token?: string): AxiosInstance => {
const getConfig = (token?: string): AxiosRequestConfig => {
const config: AxiosRequestConfig = {
baseURL: appConfig.apiUrl,
headers: {
'Cache-Control': 'no-cache',
'Content-Type': 'application/json',
Pragma: 'no-cache',
},
};
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
};
const config = getConfig(token);
const instance = axios.create(config);
// NOTE: Wire interceptors here.
return instance;
};
And finally a utility I wrote to normalize Axios errors along with other types of framework errors:
file: /src/util/service.ts
export const unwrapServiceError = (serviceMethod: string, error: any): AxiosError | Error => {
if (error.response) {
// utilize more robust AxiosError
const e = error as AxiosError;
if (e.response!.status === HttpStatusCodes.BadRequest && !e.response!.data?.message) {
e.response!.data = e.response!.data || {};
e.response!.data.message = `${serviceMethod}: Not Found`;
}
return e;
}
return new Error(`${serviceMethod} error: ${error.message}`);
};
This pattern and code nearly verbatim has served me well for many, many projects.
The Rewrite#
My process for switching to the native Fetch API started with a visit to the
MDN documentation
pertaining to passing body, options, headers etc. to a fetch request. It was pretty obvious that it would not be too
difficult of a process to create a helper method to do something similar to axios.create
. I opted just to
create the request body and explicitly pass it to fetch in the service so that it is clear that we are using the
native API.
file: /src/helpers/service.ts
import { ACCESS_ERROR, GENERIC_SERVICE_ERROR } from '../constants';
// for the demo this is in a `.env` file that Create-React-App is auto-wired to pick up
const apiUri = process.env.REACT_APP_API_URI;
function createRequest(method: FetchRequestType) {
return (url: string, body?: any, token?: string): Request => {
const headers: Headers = new Headers({
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
});
if (body) {
headers.append('Content-Type', 'application/json');
}
if (token) {
headers.append('Authorization', `Bearer ${token}`);
}
const init: RequestInit = {
method,
mode: 'cors',
cache: 'no-cache',
headers,
};
if (token) {
init.credentials = 'include';
}
if (body) {
init.body = JSON.stringify(body);
}
return new Request(`${apiUri}${url}`, init);
};
}
Maybe a little more verbose, but I opted for clarity when naming my exported helper functions:
file: /src/helpers/service.ts
export const createGetRequest = createRequest(FetchMethods.Get);
export const createPatchRequest = createRequest(FetchMethods.Patch);
export const createPostRequest = createRequest(FetchMethods.Post);
export const createPutRequest = createRequest(FetchMethods.Put);
export const createDeleteRequest = createRequest(FetchMethods.Delete);
Since there is a lot of repetitive boilerplate around processing a fetch response I opted for another helper method and decided
that it was a good place to take advantage of TypeScript’s generics for stronger typing when processing the response’s payload.
Since I have run into API’s that return compound responses, similar to an AxiosResponse with the payload coming in a data
field, I check for those here so I can dig the value I am looking for seamlessly out of the payload. This is probably a
good spot for fine-tuning to your specific needs.
file: /src/helpers/service.ts
export async function processApiResponse<Type>(response: Response): Promise<Type> {
if (response.ok) {
const payload = await response.json();
// see if we have a compound API response
if (payload.status && (payload.data || payload.errors)) {
if (!payload.success) {
throw payload;
}
return payload.data as Type;
}
return payload as Type;
}
if (
response.status === HttpStatusCodes.Unauthorized ||
response.status === HttpStatusCodes.Forbidden
) {
throw new Error(ACCESS_ERROR);
}
throw new Error(GENERIC_SERVICE_ERROR);
}
As we are no longer dealing with a potential AxiosError
body the error processor gets more than a little slimmer:
file: /src/helpers/service.ts
export const unwrapServiceError = (serviceMethod: string, error: any): Error => {
return new Error(
`${serviceMethod} error: ${error.errors ? error.errors[0] : error.message}`
);
};
Result#
Putting it all together I feel that it makes my service functions a bit more streamlined and easier to read:
file: /src/services/user/UsersApi.ts
import IUser from '../../@interfaces/IUser';
import { createGetRequest, processApiResponse, unwrapServiceError } from '../../helpers';
export const fetchUsers = async (): Promise<Array<IUser>> => {
try {
const response = await fetch(createGetRequest('/users'));
return await processApiResponse<Array<IUser>>(response);
} catch (e) {
throw unwrapServiceError('UsersApi.fetchUsers', e);
}
};
As always feel free to drop me a line if you see anything you feel could be improved. Thank you for dropping by. Reference Project