Axios HTTP Client Using TypeScript

Altrim Beqiri

Altrim Beqiri /

Whenever I plan to use axios on my projects I tend to create a tiny wrapper around it. By doing this I can expose just a subset of the methods and use only what I need from axios. Moreover I feel I can easily change the implementation later to use something like fetch or any other library.

Additionally as we can see from the following code we have also specified some headers as well as interceptors to inject the JWT token in headers and handle some generic errors.

// http.ts

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";

enum StatusCode {
  Unauthorized = 401,
  Forbidden = 403,
  TooManyRequests = 429,
  InternalServerError = 500,
}

const headers: Readonly<Record<string, string | boolean>> = {
  Accept: "application/json",
  "Content-Type": "application/json; charset=utf-8",
  "Access-Control-Allow-Credentials": true,
  "X-Requested-With": "XMLHttpRequest",
};

// We can use the following function to inject the JWT token through an interceptor
// We get the `accessToken` from the localStorage that we set when we authenticate
const injectToken = (config: AxiosRequestConfig): AxiosRequestConfig => {
  try {
    const token = localStorage.getItem("accessToken");

    if (token != null) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  } catch (error) {
    throw new Error(error);
  }
};

class Http {
  private instance: AxiosInstance | null = null;

  private get http(): AxiosInstance {
    return this.instance != null ? this.instance : this.initHttp();
  }

  initHttp() {
    const http = axios.create({
      baseURL: "https://api.example.com",
      headers,
      withCredentials: true,
    });

    http.interceptors.request.use(injectToken, (error) => Promise.reject(error));

    http.interceptors.response.use(
      (response) => response,
      (error) => {
        const { response } = error;
        return this.handleError(response);
      }
    );

    this.instance = http;
    return http;
  }

  request<T = any, R = AxiosResponse<T>>(config: AxiosRequestConfig): Promise<R> {
    return this.http.request(config);
  }

  get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R> {
    return this.http.get<T, R>(url, config);
  }

  post<T = any, R = AxiosResponse<T>>(
    url: string,
    data?: T,
    config?: AxiosRequestConfig
  ): Promise<R> {
    return this.http.post<T, R>(url, data, config);
  }

  put<T = any, R = AxiosResponse<T>>(
    url: string,
    data?: T,
    config?: AxiosRequestConfig
  ): Promise<R> {
    return this.http.put<T, R>(url, data, config);
  }

  delete<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R> {
    return this.http.delete<T, R>(url, config);
  }

  // Handle global app errors
  // We can handle generic app errors depending on the status code
  private handleError(error) {
    const { status } = error;

    switch (status) {
      case StatusCode.InternalServerError: {
        // Handle InternalServerError
        break;
      }
      case StatusCode.Forbidden: {
        // Handle Forbidden
        break;
      }
      case StatusCode.Unauthorized: {
        // Handle Unauthorized
        break;
      }
      case StatusCode.TooManyRequests: {
        // Handle TooManyRequests
        break;
      }
    }

    return Promise.reject(error);
  }
}

export const http = new Http();

Now that we have our http client we can use it in a service and we will get proper autocompletion and type hinting as we code.

// users.service.ts
import { http } from "./http";

export type User = {
  id: string;
  name: string;
  avatar: string;
  email: string;
};

export const fetchUsers = async (): Promise<User[]> => {
  const { data } = await http.get<User[]>("/users");
  return data;
};

export const createUser = async (user: User): Promise<User> => {
  const { data } = await http.post<User>("/users", user);
  return data;
};

export const updateUser = async (user: User): Promise<User> => {
  const { data } = await http.put<User>(`/users/${user.id}`, user);
  return data;
};

export const deleteUser = async (user: User): Promise<User> => {
  const { data } = await http.delete<User>(`/users/${user.id}`);
  return data;
};

Finally, say if we are using React we can use the service in a component and again we will get proper type hinting and autocompletion.

// UsersPage.tsx
import React, { useState, useEffect } from "react";
import { fetchUsers, User } from "./users.service";

const UsersPage: React.FC = () => {
  const [users, setUsers] = useState<User[]>();

  useEffect(() => {
    const fetchData = async () => {
      // Here we get users: User[]
      const users = await fetchUsers();
      setUsers(users);
    };

    fetchData();
  }, []);

  return (
    <div>
      {users?.map((user) => (
        <div key={user.id}>
          <img src={user.avatar} alt={user.name} />
          <div>{user.name}</div>
          <div>{user.email}</div>
        </div>
      ))}
    </div>
  );
};