The source code for this blog is available on GitHub.
Note
Top

Introduction to Axios: A Modern HTTP Client for JavaScript

Cover Image for Introduction to Axios: A Modern HTTP Client for JavaScript
Chen Han
Chen Han

Axios

Axios is a JavaScript library that provides a simple, promise-based interface for making HTTP requests. It is often used in web applications to send and receive data from servers, and can be used with both front-end JavaScript libraries like React and Angular, and back-end languages like Node.js.

There are a few advantages to using Axios:

  1. It is easy to use: Axios has a simple API with clear documentation, making it easy to get started with making HTTP requests.
  2. It is promise-based: Axios uses promises to handle asynchronous requests, which makes it easier to write code that is easy to understand and maintain.
  3. It is flexible: Axios allows you to make requests using a variety of HTTP methods, such as GET, POST, PUT, DELETE, and more. It also allows you to configure the request with a wide range of options, including headers, data, and other options.
  4. It has a strong community: Axios is a popular library with a large community of users, which means there are many resources available for learning how to use it, and it is well-maintained and updated regularly.

Overall, Axios is a reliable and powerful tool for making HTTP requests in JavaScript, and is well-suited for use in web applications of all sizes.

interceptors

An interceptor is a function that is called before or after a request or response is made by Axios. They can be used to modify requests and responses, or to perform actions based on the request or response.

One way interceptors can be used is to add or modify headers on requests. For example, you might use an interceptor to add an authorization header to every request made by Axios. You can also use interceptors to handle errors, by catching failed requests and doing something with the error before it is passed along to the calling code.

Another use case for interceptors is to modify or transform the data in a request or response. For example, you might use an interceptor to convert all responses to a consistent format, or to transform data before it is sent as a request.

To use interceptors with Axios, you can use the axios.interceptors.request.use() and axios.interceptors.response.use() methods. These methods take a function as an argument, which will be called with the request or response object as an argument. You can then modify the object and return it, or return a new object to be used in place of the original.

It is worth noting that interceptors are added to a global array, and will be called for every request or response made by Axios. This means that you should be careful to ensure that your interceptors are specific to the needs of your application, and do not cause unintended side effects.

Overall, interceptors can be a powerful tool for modifying and handling requests and responses in Axios, but it is important to use them responsibly in order to avoid any unintended consequences.

Here are a few examples of how you might use interceptors in Axios:

  1. Adding a header to every request:
// Add a header to every request made by Axios
axios.interceptors.request.use(config => {
  config.headers['Authorization'] = 'Bearer abc123';
  return config;
});
  1. Handling errors:
// Catch errors for any requests made by Axios, and log them to the console
axios.interceptors.response.use(null, error => {
  console.error('Axios error:', error);
  return Promise.reject(error);
});
  1. Modifying the data in a request:
// Convert all data objects in requests to a string before they are sent
axios.interceptors.request.use(config => {
  config.data = JSON.stringify(config.data);
  return config;
});
  1. Modifying the data in a response:
// Convert all data in responses to camelCase
axios.interceptors.response.use(response => {
  response.data = camelizeKeys(response.data);
  return response;
});

Keep in mind that these are just a few examples, and there are many other ways you can use interceptors in Axios. It is also possible to chain multiple interceptors together, or to remove interceptors from the global array if they are no longer needed.

Services

I wrapped the API in services using the following code:

import axios from 'axios';
import logger from './logService';

// request interceptors
axios.interceptors.request.use((config) => {
  // Do something before request is sent
  return config;
}, (error) => Promise.reject(error));

// The place to handle error.
axios.interceptors.response.use(
  (response) => {
    // Do something when received the response
    return response;
  },
  (error) => {
    const clientSideError =
      error.response &&
      error.response.status >= 400 &&
      error.response.status < 500;

    // client side error
    if (clientSideError) {
      logger.requestLog(error);
    }

    // server side error
    if (!clientSideError) {
      logger.log(error);
    }

    if (typeof window !== 'undefined') {
      // client side
      console.log('client side error');
    } else {
      // server side
      console.log('server side error');
    }

    return Promise.reject(error);
  },
);

/* default timeout */
axios.defaults.timeout = 20000;
/* use json as default post action */
axios.defaults.headers.post['Content-Type'] = 'application/json';
axios.defaults.headers.post['Accept'] = 'application/json';
/* use json as default post action */
axios.defaults.headers.put['Content-Type'] = 'application/json';
axios.defaults.headers.put['Accept'] = 'application/json';

export default {
  get: axios.get,
  post: axios.post,
  put: axios.put,
  patch: axios.patch,
  delete: axios.delete,
  instance: axios,
};

The following services inherit from the service defined above and make HTTP requests using Axios. Each service sets its own default headers and base URL (if applicable). The services also destructure specific HTTP methods from the Axios instance, such as get, post, and put.

  • The first service sets a default Authorization header and exports the get, post, put, and headers methods.
  • The second service sets a default X-Auth-Token header and exports the get, post, put, delete, and headers methods.
  • The third service sets a default I-API header and base URL, and exports the get, post, put, patch, and delete methods.
import HttpService from "./httpService";

/* Ship Station default */
HttpService.instance.defaults.headers.common["Authorization"] = `Basic ${process.env.SS_Auth}`;
/* Verb */
const { get, post, put, headers } = HttpService.instance;

export default { get, post, put, headers };
import HttpService from "./httpService";

/* Big Commerce default */
HttpService.instance.defaults.headers.common["X-Auth-Token"] =
  process.env.BIGCOMMERCE_TOKEN;
/* Verb */
const { get, post, put, delete: del, headers } = HttpService.instance;

export default { get, post, put, delete: del, headers };
import HttpService from "./httpService";
import { addressLookUpUrl } from "@/lib/addressLookUpUrl";

HttpService.instance.defaults.headers.common["I-API"] =
  process.env.REG_APIKEY;
HttpService.instance.defaults.baseURL = addressLookUpUrl
/* Verb */
const { get, post, put, patch, delete: del } = HttpService.instance;

export default { get, post, put, patch, delete: del };

By the way, before I made the article, I put them in chatGPT to let him helps me to refactor the code. I point what he pretty much to do for me:

  • Replaced the function keyword with the => syntax for the request and response interceptors, as this is more concise and easier to read.
  • Used the typeof operator to check if the window object is defined, instead of using !!globalThis.window. This is more robust, as it will work in all environments, including server-side rendering.
  • Added a return statement to the error handler for the request interceptor, so that the rejected promise is returned.
  • Changed the parameter name for the response interceptor from request to response, as this is more descriptive and accurate.
  • Removed the unnecessary const keyword from the destructured variables.
  • Replaced the delete destructured variable with del, as delete is a reserved word in JavaScript and cannot be used as a variable name.

with cookies

To send cookies with an HTTP request using Axios, you can use the withCredentials property in the config object. This property, when set to true, includes cookies in the request.

Here's an example of making an HTTP GET request to a server that requires cookies:

import axios from 'axios';

axios.get('http://example.com/api/endpoint', {
  withCredentials: true
})
.then(response => {
  // handle success
})
.catch(error => {
  // handle error
});

You can also set the withCredentials property globally for all requests by default by modifying the defaults property of the axios instance:

axios.defaults.withCredentials = true

Note that the server you are making the request to must also be configured to allow credentials in order for this to work. The server can indicate this by setting the Access-Control-Allow-Credentials header to true.

Awesome Api Handler

In the example where promise is rejected (i.e., an error occurs), the catch block will catch the error and log it to the console. By returning [null, error] in this case, the function informs the caller that an error has occurred and provides the error object so that it can be further processed.

import { AxiosResponse } from "axios";

export async function awesomeApiResponse (promise: AxiosResponse) {
  try {
    const data = await promise;
    return [data, null];
  } catch (error) {
    console.log(error);
    return [null, error];
  }
}

How to use

const response = await axios.get('https://jsonplaceholder.typicode.com/users');
const [data, error] = await awesomeApiResponse(response);

if (error) {
  console.log('Error occurred:', error);
} else {
  console.log('Data fetched successfully:', data);
}

In this example, the fetchData function is using the axios library to make a GET request to a public API. Once the response is received, it is passed as an argument to the awesomeApiResponse function.

The awesomeApiResponse function checks if the promise resolves successfully or if there is an error. If the promise resolves successfully, the data is returned as the first item in an array, with null as the second item. If there is an error, null is returned as the first item in the array, and the error is returned as the second item.

In the fetchData function, the array returned by awesomeApiResponse is destructured into two variables, data and error. If there is an error, the error message is logged to the console. Otherwise, the fetched data is logged to the console.

Retry

The retry package is a JavaScript library that provides an easy way to retry asynchronous operations that may fail due to transient errors. It allows you to specify a function that performs an asynchronous operation and automatically retries it a specified number of times if it fails, based on configurable backoff and retry strategies.

With retry, you can define the maximum number of retries, the interval between each retry, and a custom retry strategy that determines when to retry based on the error thrown by the operation. It also supports a range of different backoff strategies, including exponential backoff, which increases the interval between each retry exponentially with each retry.

retry is useful for scenarios where you need to perform an operation that may fail due to network issues, service downtime, or other transient errors, and you want to make sure that the operation eventually succeeds. It can be used in a wide range of applications, such as retrying failed HTTP requests, connecting to a database, or performing file I/O.

Here's an example of how you can use the retry package to retry a failing operation with exponential backoff:

import retry from "retry";

operation.attempt(async () => {
  try {
    const details = await actions.order.capture();
    await paymentFlow(details);
  } catch (e) {
    console.error("Error capturing payment: ", e);
    if (operation.retry(e)) {
      console.log("Retrying payment capture...");
      return;
    }
    new Error("Max retries reached, payment capture failed");
  }
});

Refactoring HTTP Services

In this article, we will explore how to refactor an HTTP service from JavaScript to TypeScript, using class-based structure and generics for type safety. We will start with a JavaScript implementation and then move step by step to improve it with TypeScript, classes, and generics.

The JavaScript Version

Here's an example of an HTTP service implementation in JavaScript using axios:

import axios from "axios";
import logger from "./logService";

// request interceptors
axios.interceptors.request.use(
  (config) => {
    // Do something before request is sent
    return config;
  },
  (error) => Promise.reject(error)
);

// The place to handle error.
axios.interceptors.response.use(
  (response) => {
    // Do something when received the response
    return response;
  },
  (error) => {
    const clientSideError =
      error.response &&
      error.response.status >= 400 &&
      error.response.status < 500;

    // 🍥is server-side
    const isServerSide = typeof window === "undefined"

    // 4xx error
    if (clientSideError) {
      logger.request(error, isServerSide);
    }

    // 5xx error
    if (!clientSideError) {
      logger.request(error, isServerSide);
    }

    return Promise.reject(error);
  }
);

/* default timeout */
axios.defaults.timeout = 20000;
/* use json as default post action */
axios.defaults.headers.post["Content-Type"] = "application/json";
axios.defaults.headers.post["Accept"] = "application/json";
/* use json as default post action */
axios.defaults.headers.put["Content-Type"] = "application/json";
axios.defaults.headers.put["Accept"] = "application/json";

export default {
  get: axios.get,
  post: axios.post,
  put: axios.put,
  patch: axios.patch,
  delete: axios.delete,
  instance: axios,
};

Introducing TypeScript and Classes

Now let's start converting the JavaScript implementation to TypeScript. We will use a class-based structure for better organization and type safety.

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import logger from "./logService";

// Create an HttpService class
class HttpService {
  private axiosInstance: AxiosInstance;
  constructor() {
    this.axiosInstance = axios.create();
    
    this.axiosInstance.interceptors.request.use(
      (config: AxiosRequestConfig) => config,
      (error) => Promise.reject(error)
    );
    
    this.axiosInstance.interceptors.response.use(
      (response: AxiosResponse) => response,
      (error) => {
        const clientSideError =
          error.response &&
          error.response.status >= 400 &&
          error.response.status < 500;
        
        // 🍥is server-side
        const isServerSide = typeof window === "undefined"
  
        // 4xx error
        if (clientSideError) {
          logger.request(error, isServerSide);
        }
  
        // 5xx error
        if (!clientSideError) {
          logger.request(error, isServerSide);
        }
  
        return Promise.reject(error);
      }
    );
    
    this.axiosInstance.defaults.timeout = 20000;
    this.axiosInstance.defaults.headers.post["Content-Type"] = "application/json";
    this.axiosInstance.defaults.headers.post["Accept"] = "application/json";
    this.axiosInstance.defaults.headers.put["Content-Type"] = "application/json";
    this.axiosInstance.defaults.headers.put["Accept"] = "application/json";
  }
  
  async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.axiosInstance.get<T>(url, config);
  }
  
  async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.axiosInstance.post<T>(url, data, config);
  }
  
  async put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.axiosInstance.put<T>(url, data, config);
  }
  
  async patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.axiosInstance.patch<T>(url, data, config);
  }
  
  async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.axiosInstance.delete<T>(url, config);
  }
  
  get instance(): AxiosInstance {
    return this.axiosInstance;
  }
}

export default new HttpService();

Adding Generic Types for Enhanced Type Safety

In this section, we will update the class methods to support generic types, which provide a more flexible and type-safe way to use the HTTP methods.

get(url: string, config?: AxiosRequestConfig) {
  return this.axiosInstance.get(url, config);
}

post(url: string, data?: any, config?: AxiosRequestConfig) {
  return this.axiosInstance.post(url, data, config);
}

put(url: string, data?: any, config?: AxiosRequestConfig) {
  return this.axiosInstance.put(url, data, config);
}

patch(url: string, data?: any, config?: AxiosRequestConfig) {
  return this.axiosInstance.patch(url, data, config);
}

delete(url: string, config?: AxiosRequestConfig) {
  return this.axiosInstance.delete(url, config);
}

First, update the get method definition in the HttpService class:

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

Similarly, update the other methods (post, put, patch, and delete) to support generics:

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

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

async patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
  return this.axiosInstance.patch<T>(url, data, config);
}

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

With these changes, you can now use the generic type with your methods without any type. Now you should be able to use the generic type with your methods without any type error:

await httpService.get<Category.GetDropdownCategoryResponse>(/* your_url_here */);
await httpService.get<Product.GetProductResponse>(/* your_url_here */);

In this article, we have walked through the process of refactoring an HTTP service from JavaScript to TypeScript, switching from a functional to a class-based structure, and employing the power of generic types for better type safety. These changes allow you to write more maintainable, clean, and type-safe code, reducing the likelihood of runtime errors and making your development experience more enjoyable.

Why is it better for class-based components over functional-based components

In this specific example, I've chosen to use a class-based pattern over a functional pattern for the HTTP service for a few reasons:

  1. Encapsulation: By using a class, we can encapsulate the behavior and state of the HTTP service. It provides a clear separation of concerns and a more organized structure. This makes it easier to manage and reason about the code.
  2. Reusability: A class-based pattern allows you to easily create multiple instances of the HTTP service if needed, each with its own configuration and state. This could be helpful in cases where you need to interact with different APIs or services with different settings.
  3. Extensibility: In a class-based pattern, it is easier to extend the functionality of the HTTP service, by adding new methods or overriding existing ones. This can be beneficial when you want to customize the service for specific use cases.
  4. Type Safety: Using a class with TypeScript enables better type safety for both the internal state and the exposed methods. By clearly defining the types of the instance variables and the input/output of the methods, you can catch potential errors during development rather than at runtime.
  5. Easier testing: By using a class-based pattern, it becomes easier to create isolated instances of the HTTP service for testing purposes. You can instantiate a new instance of the service with specific configurations or even mock parts of it, allowing for more robust and reliable testing.

These benefits can lead to a more maintainable, flexible, and easier-to-understand codebase. However, it's worth noting that there are cases where a functional pattern might be more suitable, depending on the specific requirements and constraints of a project. It's essential to evaluate the trade-offs and choose the approach that best fits your needs.

Rewritten in functional-based

In this functional implementation, I have removed the HttpService class and created a separate function createAxiosInstance to initialize the AxiosInstance. Then, I exposed the methods get, post, put, patch, delete, and instance as properties on the httpService object.

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import logger from "./logService";

const createAxiosInstance = (): AxiosInstance => {
  const axiosInstance = axios.create();

  axiosInstance.interceptors.request.use(
    (config: AxiosRequestConfig) => config,
    (error) => Promise.reject(error)
  );

  axiosInstance.interceptors.response.use(
    (response: AxiosResponse) => response,
    (error) => {
      const clientSideError =
        error.response &&
        error.response.status >= 400 &&
        error.response.status < 500;

      const isServerSide = typeof window === "undefined";

      if (clientSideError) {
        logger.request(error, isServerSide);
      }

      if (!clientSideError) {
        logger.request(error, isServerSide);
      }

      return Promise.reject(error);
    }
  );

  axiosInstance.defaults.timeout = 20000;
  axiosInstance.defaults.headers.post["Content-Type"] = "application/json";
  axiosInstance.defaults.headers.post["Accept"] = "application/json";
  axiosInstance.defaults.headers.put["Content-Type"] = "application/json";
  axiosInstance.defaults.headers.put["Accept"] = "application/json";

  return axiosInstance;
};

const axiosInstance = createAxiosInstance();

const httpService = {
  get: <T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> =>
    axiosInstance.get<T>(url, config),

  post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> =>
    axiosInstance.post<T>(url, data, config),

  put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> =>
    axiosInstance.put<T>(url, data, config),

  patch: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> =>
    axiosInstance.patch<T>(url, data, config),

  delete: <T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> =>
    axiosInstance.delete<T>(url, config),

  instance: axiosInstance,
};

export default httpService;
© 2024 WOOTHINK. All Rights Reserved.
Site MapTerms and ConditionsPrivacy PolicyCookie Policy