Building a reusable HTTP Client logic

An Axios Demo

Building a reusable HTTP Client logic

Summary

Unless you’re building a static HTML website, there’s every single chance that your application will rely on data fetched through an API to power certain functionalities. A production-ready application might even have up to 50 or more API endpoints to interact with.

As the application grows and more functionalities are being added, the API endpoints being consumed are also expected to increase, leading to a duplication of the blocks of code being used to make the HTTP requests. How can developers ensure reusability in their HTTP logic, to prevent unwanted code duplication?

Within this tutorial, we will explore making our HTTP logic reusable while using the popular Axios library. While the reusable logic in this tutorial will be applied to a React.js web application, you can also apply them to any other application making HTTP requests.

Prerequisites

This tutorial will contain several hands-on steps. To follow along, it is recommended that readers have a prior understanding of;

  • The HyperText Transfer Protocol.
  • The Javascript Programming language and the ES6 syntax.
  • Object-oriented programming.
  • React and React Hooks.

Getting started

Bootstrap a new React.js application using the command below. The React.js application will contain most of the work that we will be doing within this tutorial.

npx create-react-app api-app --template typescript

The command above will create a React.js application named “api-app” using the typescript template.

Change directory (cd ) into the api-app folder and (&&) execute the command below to start the application api-app application.

cd api-app && yarn start

Using your web browser, navigate to the React.js application running at http://localhost:3000.

At this point, the api-app only displays the boilerplate view above. In the next sections, you will begin to use Axios in the api-app application.

What is Axios

Axios is a promise-based HTTP client for the browser and Node.js. It provides a syntax that takes away a lot of boilerplate. With axios, we can create our own instance(s) with different properties such as baseURL (the consistent part of the API URL) and request headers without any hassle. We will see how several logic can be made easier and most especially, how much cleaner and reusable our code can get.

Run the command below to install Axios into the api-app project;

npm  i  axios

Next, we will proceed to create an instance of Axios that can be reused throughout the api-app project.

Creating an Axios instance

Create a new file called AxiosConfig.ts in src/api. It is important to choose a uniform convention for naming files and folders. In this article, I use “PascalCase” and “kebab-case” for naming files and folders respectively.

If you think about an API, obviously some parameters will be consistent for each request. The base URL and the request headers will are some parameters that might be different, depending on which servers you’re communicating with. For protected endpoints, you might need to have an authentication token attached to each request. It is logical to define all these values as properties of the class, without having to always pass them to each request. Alright, let’s write some typescript.

Copy the code below into the AxiosConfig.ts file you created.

// src/api/AxiosConfig.ts

class AxiosConfig {
  private _baseUrl = '';
  private _headerConfigs?: AxiosRequestHeaders = {};
  private _configs: AxiosrequestConfig = {
    baseUrl: this._baseUrl,
    timeout: 60000,
    headers: this._headerConfigs
  };
  private _instance?: AxiosInstance;

  public get axiosInstance() {
    return this._instance;
  }

  constructor() {
    // create a new axios instance and assign to property
    this._instance = axios.create(this._configs);
  }
}

export default AxiosConfig;

The code above creates a new class called AxiosConfig with four private fields, a get accessor for the axiosInstance, and a constructor method. AxiosRequestConfig and AxiosRequestHeaders are types exported from axios.

The AxiosRequestConfig type represents a request’s interface, while the AxiosRequestHeaders type is an interface representing a request’s header method. The constructor method will set the _instance value whenever the class is instantiated, we will see how we can create multiple instances with different properties.

Note that keywords like private and public are only possible because this is a typescript file, to replicate a private property in javascript, start the property name with a hashtag, #, but all properties are public by default.

Adding Interceptors

Interceptors are used to block API requests temporarily before sending them and, also maybe the response. Interceptors are useful for adding logic before, and/or after making an HTTP request. Let’s see how we add interceptors to our Axios instance.

We need to update the constructor of the AxiosConfig class to now look like the code below, replacing the previous constructor declaration;

constructor(baseUrl: string, headers?: AxiosRequestHeaders) {
  // set class baseUrl to the initializing parameters
  this._baseUrl = baseUrl;

  // if headers are passed, then add it to the the existing config
  if (headers) {
    this._headerConfigs = Object.assign({}, this._headerConfigs, headers);
  }

  // create a new axios instance and assign to property
  this._instance = axios.create(this._configs);

  // intercept the request and run any actions
  // request will not run if this fails
  this._instance?.interceptors.request.use(
    (config) => {
      return config;
    }.
    (error) => {
      return Promise.reject(error);
    }
  );

  // intercept the response and handle the data
  // the question mark syntax here is an optional chaining technique
  this._instance?.interceptors.response.use(
    (response) => {
      return response;
    }.
    (error) => {
      return Promise.reject(error);
    }
  );
}

This is a more advanced model of what we had and some important changes from what we had include;

  1. The constructor method of the class now takes two arguments, a string representing the baseUrl and a header object. This allows the AxiosConfig class to be instantiated with different baseUrl and customized headers

  2. Line 6 checks if a headers object was passed and using the assign method copies the new headers into the existing request headers. This change along with the new arguments is useful for declaring multiple instances but with different base properties.

  3. Then we define the request and response interceptors. The created axiosInstance has the interceptors object which contains at least two fields request and response. The use method on both fields is what defines the interceptors for request and response. The interceptors.request.use takes a config argument that contains all data related to the request and an error object in case it fails. You could for example add an authentication token to the headers before every request. The response argument on the interceptors.response does the same too, except it works for the response parts.

Creating the HttpClient

Now we need to create one more helper class to complete our cleanup. In a usual application, there will be different use cases for data, and based on this, there are various HTTP request methods available.

We need to create a class that defines this logic and passes the exact parameters needed. This is what this helper class will do for us.

Create a new file named HttpClient.ts in src/api and copy the code below into it.

export class HttpClient {
  public static get<ResponseType>(endpoint: string) {
    return axios.axiosInstance?.({
      method: HttpMethods.GET,
      url: endpoint
    }).then(({ data }) => data as ResponseType);
  }

  public static post<RequestType = any, ResponseType = unknown>(
    endpoint: string,
    body?: requestType 
  ) {
    return axios.axiosInstance?.({
      method: HttpMethods.POST,
      url: endpoint,
      body?: RequestType
    }).then(({ data }) => data as ResponseType);
  }
}

Let’s gradually consider each block of the code above.

  1. We defined a HttpClient class with two static methods, static methods are unlike normal methods of a class, they don’t need the class to be instantiated before they can be called, get and post. Notice how these are named after the actual request methods. If you need to add a ‘delete’ endpoint, you could easily add that here and expect the right parameters.
  1. The get and post methods both expect an endpoint. Similarly, they both expect generic types to allow for easier type checking. A notable difference between them is the extra parameters, the post method expects a body argument since the post method is usually for creating data on the database.

Cleaning up the HttpClient

The way we have written the HttpClient class right now is pretty repetitive and it’s very easy to see it get worse. For instance, the get method can be used to filter data from the API while, but if the data has query parameters, it’s easier to pass the query parameters as an object and then concatenate them in the HttpClient, that way we get to do it in one place.

Replace the existing code in the HttpClient.ts file with the content of the code block below;

export class HttpClient {
  private static async request<ResponseType = any, RequestType = any>(
    method: HttpMethods,
    endpoint: string,
    request?: ApiRequest<RequestType>
  ) {
    return await axios.axiosInstance?.({
      method,
      url: endpoint,
      data: request?.body
    }).then(({ data }) => {
      return data as ResponseType;
    });
  }

  public static get<ResponseType>(
    endpoint: string, 
    query?: Record<string, React.ReactText>
  ) {
    return this.request<ResponseType>(HttpMethods.Get, endpoint, { query });
  }

  public static post<RequestType, ResponseType = unknown>(
    endpoint: string,
    body?: RequestType
  ) {
    return this.request<ResponseType>(HttpMethods.Post, endpoint, { body });
  }
}

To write a cleaner version of the HttpClient class, we performed the following actions explained below in the HttpClient.ts file;

  1. We removed the request itself (the axios.axiosInstance) to a private method defined as request.
  1. The get method now takes an optional query argument of type Record (basically, an object of key-value pair).

This HttpClient class makes sure you pass the correct data for each method and defines a simple layer to prepare the parameters for the API request. For instance, in a GET request that takes several query parameters, it is useful to pass the queries as a key-value pair object and convert the whole object into a string. In that case, a use case for the request method will be to add such logic.

Copy the conditional statement in the code below into the request method of the HttpClient class;

if (request?.query) {
      const query = Object.entries(request.query)
        .filter(([, value]) => typeof value !== 'undefined')
        .map(([ field, value]) => field + '=' + value)
        .join('&');

        endpoint += query ? '?' : ''; 
    }

This checks if there is a query property on the request object, creates an array of key-value tuples (something like [[key, value]]), removes undefined value, returns an array of strings, and joins them all with an ampersand. In short, it gives us a new endpoint that looks like, ?name=Jamal&cause=react.

Making the request from a component

This final piece is the most important aspect, we could spend the whole morning talking about it. Making the API request is basically, opening up your app.js file and calling ApiClient.get, it’s a promise and you’ll be able to handle the response that comes through. But what that will do is make your code super buggy and hard to follow. Create a new react component in a new file. You can fork the sandbox from here, for a good way to structure this project.

    export const DisplayAnimeGifs = () => {
      const [coinDetails, setCoinDetails] = React.useState<any[]>([]);

      React.useEffect(() => {
        HttpClient.get<any[]>('coins/market', {
          vs_currency: 'usd',
          per_page: 20,
          category: 'aave-tokens'
        }).then((data) => setCoinDetails(data as any[]));
      }, [])

      return (
        <Wrapper>
          <ul>{}</ul>
        </Wrapper>
      )
    }

This is a small component that utilizes the HttpClient.get method and is the approach I have seen in many codebases. The DisplayAnimeGifs component above performs the following actions;

  1. It defines the states that keep that response data from our API requests.

  2. The react useEffect hook can be used to fetch the data on mount. So we call the HttpClient.get method, passing the desired query parameters. Notice how easy it is to use the query argument in this request here, we could easily make any of the values dynamic.

A not-so-obvious issue with this approach is, that if there are additional 1 or 2 requests on this page, it may start getting hard to read. A fix for this to ensure clarity and of course, manageability is to extract the API calling logic into a custom hook. If you want to use redux, you can extract the API logic into redux-thunk.

This article assumes the usage of local state management. In the same folder, create a new file DisplayAnimeGifs.hooks.ts. and copy the code below into it

Hint: To make projects manageable, it is better to keep related code together, so the hook, the component and the styles should be together.

I am using the coingecko free API and you can use any APIs you like,

import { useEffect, useState } from "react";
    import { HttpClient } from "../api/HttpClient";

    // it is customary to name custom hooks this way,
    // starting with use
    export const useApiHook = () => {
      const [isLoading, setloading] = useState(false);
      const [coinDetails, setCoinDetails] = useState<any[]>([]);

      const fetchCoinDetails = async () => {
        const queryParams = {
          vs_currency: "usd",
          per_page: 20,
          category: "aave-tokens"
        };
        setloading(true);
        await HttpClient.get<any[]>("coins/markets", queryParams)
          .then((data) => setCoinDetails(data as any[]))
          .finally(() => setloading(false));
      };

      useEffect(() => {
        fetchCoinDetails();
      }, []);

      return {
        coinDetails,
        isLoading
      };
    };

This is a custom hook that can be used in our component.

  1. Lines 7 & 8 define two states, isLoading and coinDetails which are boolean and array respectively.
  1. We have our HttpClient.get call. If you notice, we basically extracted the logic in the previous component into this custom hook.
  1. This custom hook then returns the two states defined.

Our DisplayAnimeGif component now looks much simpler. Let’s see how we can use the custom hook in our component.

Replace the content of the DisplayAnimeGif.tsx component with the code below;

export const DisplayAnimeGifs = () => {
      const { coinDetails, isLoading } = useApiHook();

      return (
        <Wrapper>
          <ul>{isLoading ? 'Page is loading' : coinDetails.map((coin) => (
            <li key={coin.id}>{coin.name}</li>
          ))}</ul>
        </Wrapper>
      )
    }

This is how we can get the data to our component in a clean way. If the app gets very big, it is super easy to move the logic into a much larger state and the components still continue working hence, creating a robust codebase and application.

Conclusion

We have seen how we can make HTTP requests with axios, we learned what interceptors are and how to add interceptors to our axios instance. No matter how good your app looks, it’s huge stress if it’s hard to manage or work with.

One way to manage against that is to extract functionalities into reusable codes which ensures a single source of truth for the app’s logic. An example is defining a logic around API services, as opposed to having to rewrite it all over the component, and also using custom hooks to extract the component logic to somewhere more manageable. The code for this project is available on codesandbox and also on Github.