Axios v Fetch for API interactions

It is outside the code of this documentation to force a decision upon the developer as to which method for API interactions should be employed. Instead we have provided some comparisons, to assist in the decision making

fetch

fetch is a native API available in modern browsers. “Native” in the context of browser APIs means that the feature is built into the browser’s JavaScript environment without the need for external scripts or plugins. It’s a standard part of the environment. For instance, fetch is now a standard part of the Web Platform API, which modern browsers aim to support. While it’s native to modern browsers, it’s true that older browsers like IE require a polyfill (a script that provides the functionality of a newer feature in older browsers that don’t natively support it).

Pros:

  • Native: Doesn’t require any additional packages or libraries to use.

  • Promise-based: Easily use with async/await.

  • Readable syntax: Especially for simple GET requests.

Cons:

  • Error Handling: Doesn’t reject on HTTP error statuses (e.g., 404 or 500) but only on network errors or request failures.

  • Features: Some advanced features (like request timeout, request cancellation) are not natively supported or require additional work.

  • JSON Parsing: Requires an additional step to parse JSON (response.json()).

axios

axios is a popular third-party HTTP client library. Acknowledged as an industry standard.

Pros:

  • Error Handling: Rejects the promise on HTTP error statuses, which can simplify error handling.

  • Interceptors: Provides the ability to intercept requests and responses before they are handled or sent.

  • Timeouts: Built-in support for request timeouts.

  • Cancellable Requests: Supports request cancellation using the CancelToken feature.

  • Automatic JSON Parsing: Automatically parses JSON data from responses.

  • Wider Browser Support: Has built-in XHR handling which provides compatibility with older browsers.

  • Transforms: Allows data to be transformed before it’s sent or after it’s received.

Cons:

  • External Dependency: Adds an additional dependency to your project.

  • Size: While not massive, it’s still larger than the native fetch.

API Interactions with FETCH

The useAPIClient custom hook has been provided, which provides a clean and consistent way to interact with API’s For guidance on the usage of this hook, please refer to the documentation provided for that component

API Interactions with AXIOS

Using axios required the inclusion of the axios library. It was decided not to enforce this inclusion upon all uses of this library, so instead the usage has been provided by a number of code snippets that should added to the the parent application. This will result in the creation of a custom hook that can be used for axios interactions.

If you already have an axios client, skip to the Add an axios interceptor section to add tokens from the logged in account to requests.

Creating the axios client

1. Install the axios dependency

Execute the following command, which will add the dependency to the application

yarn add axios

2. Add location for the axiosAuthCLient

It is recommended to add this into the services folder of your application, as this folder is intended for use with external interactions

services
   axios
      axiosAuthClient

3. Create the standard axiosAuthClient file

This file will be used by all axios calls, so it is recommended to have an appropriate name to indicated that it includes authentication

axiosAuthClient.tsx

4. Add the basic code for an axios client

The following code snippet is standard for both standard, and authenticated axios usage

import axios from 'axios';

 export const useAxiosClient = (baseURL: string) => {
   const { instance } = useMsal();

   const axiosClient = axios.create({
     baseURL,
     headers: {
       Accept: 'application/json',
       'Content-Type': 'application/json'
     }
   });

 export default useAxiosClient;

Tip

If there is a requirement for use of axios without authentication, it is recommended to create a second axios Client.

5. Rename the name of the axiosClient

As the code will be providing authentication, it is recommended that it is adjusted to reflect that

useAxiosClient ----- ( becomes ) -----> useAxiosAuthClient
axiosClient ----- ( becomes ) -----> axiosAuthClient

Add an axios interceptor

The following code snippet intercepts axios calls using this client and adds the required authentication functionality This interceptor can then be adjusted for the specific needs of the application - It includes code to allow the use of http during local development, but inhibits its use once deployed. - Fetching of a new token if the current one has expired is done automatically

export enum LogLevel {
    Error,
    Warning,
    Info,
    Verbose,
    Trace
}

export const loginRequest = {
    scopes: ['user:read']
};

...

const getToken = useGetToken();

// Can also import globalAxios and set the interceptors on that rather than the axiosAuthClient
axiosAuthClient.interceptors.request.use(
    async request => {
      const isLocalhost =
        window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
      const isHttp = request?.baseURL?.startsWith('http://');

      // Correct logic: Allow HTTP ONLY on localhost.  Otherwise, force HTTPS.
      if (isHttp && !isLocalhost) {
        return Promise.reject('HTTP is not allowed except on localhost.');
      } else if (
        isHttp &&
        !isLocalhost &&
        request.baseURL &&
        !request.baseURL.startsWith('https://')
      ) {
        request.baseURL = request.baseURL.replace('http://', 'https://');
      }

      const accessToken = await getToken(loginRequest);
      request.headers['Authorization'] = `Bearer ${accessToken}`;
      return request;
    },
    error => Promise.reject(error)
);

Once the steps above have been completed, the axiosAuthClient can be used for all API calls requiring authentication

Using the axios client within the codebase

With the custom hook for the axios authenticated client in place in the code, next it needs to be implemented. Below are the steps that should be employed. In this example the requirement is to add a POST to an API

Tip

It is advised that all the steps are reviewed before coding to ensure that the process is fully understood

1. Add location and file for the axios POST function

It is recommended to add this into the services folder of your application, as this will interact with external APIs

services
   axios
      postToAPI
         postToAPI.tsx

2. Function is written to make use of the custom hook

Add the following into the postToAPI.tsx file.

import { AxiosResponse } from 'axios';
import { useAxiosAuthClient } from '@services/axios/axiosAuthClient/axiosAuthClient';

const PostToAPI = async (
  axiosClient: ReturnType<typeof useAxiosAuthClient>, // Add the authAxiosClient parameter
  options: object
): Promise<AxiosResponse> => {

  try {
    const response = await axiosClient.post(`/somePath`, options);
    return response;
  } catch (error) {
    console.error('Error posting:', error);

    if (error.response) {                                      // Server error
      throw error.response;                                    // Re-throw the AxiosResponse error for the caller to handle
    } else if (error.request) {                                // Request error (no response)
      throw new Error('No response received from the server'); // Re-throw a custom error
    } else {                                                   // Client-side error
      throw error;                                             // Re-throw the original error
    }
  }
};

export default PostToAPI;

3. Use the function

Add the function into the codebase into the location that it is required

import PostToAPI from '@services/axios/postToAPI/postToAPI';

...

const authAxiosClient = useAxiosAuthClient(... BASE URL OF THE APPLICATION ...);

...

const result = await PostToAPI(authAxiosClient, options);
if (![200, 201].includes(result.status)) {
  // Do some error handling using `result.data.message`
} else {
  // Do something with the information contained in `result.data`
  ... do some action
}