Axios HTTP Client Using TypeScript

Altrim Beqiri

Altrim Beqiri /

Whenever I plan to use axios on my projects I typically develop a small wrapper around it. This approach allows me to expose only a subset of the methods and utilize only the parts I need from axios. Moreover I feel I can easily change the implementation details in the future to use fetch or any other library underneath without affecting its' usage.

Additionally as illustrated in the following code snippet we have also specified default headers that we can append to the requests, an interceptor to inject the JWT token in headers and another one to handle few 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 injectToken function to inject the JWT token through an interceptor
// We get the `accessToken` from the `localStorage` which is stored during the authentication process
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();

With our http client set up, we can integrate it into a service, which will provide enhanced autocompletion and type hinting during coding.

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

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

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

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

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

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

Finally, say if we are using React incorporating the service into a component will ensure robust type hinting, type checking, and autocompletion features.

// 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>
  );
};