Building a Resilient Axios API Service with Error Handling and Notifications
Tags: react, typescript, axios, service
Published: 2023-10-15 15:12:20


In modern web development, robust and reliable APIs are crucial for delivering seamless user experiences. To achieve this, we often turn to popular libraries like Axios, which simplifies HTTP requests. In this article, we'll explore a structured Axios API service that provides error handling and notification capabilities. This service can be integrated into your web applications to enhance the user experience and streamline error management.

# 1. Setting Up Axios

The foundation of our API service is Axios, a widely-used JavaScript library for making HTTP requests. By creating an Axios instance, we can configure common settings and apply interceptors to handle responses and errors. This instance is responsible for sending requests and receiving responses from your API.

```ts
import axios, { AxiosError, AxiosRequestConfig } from 'axios';

const axiosInstance = axios.create({
  baseURL: import.meta.env.VITE_BASE_SERVICE_URL,
  // Add any other global configurations here.
});
```

# 2. Error Handling

To build a resilient API service, we need to handle errors gracefully. Axios allows us to intercept responses and errors using the interceptors feature. In this case, we're using a response interceptor to capture and log errors. If the request is canceled, we avoid showing an error `notification`.

```ts
axiosInstance.interceptors.response.use(
  (response) => response,
  (error: AxiosError) => {
    if (axios.isCancel(error)) {
      window.console.log('Request canceled', error.message);
    }

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

# 3. Adding Notification Support

Notifications are an essential part of user-friendly web applications. We've created a `notification` utility that leverages the `react-toastify` library to display notifications for different types of messages (info, success, warning, and error). This utility simplifies the process of providing feedback to users about API interactions.

```ts
import { toast } from 'react-toastify';

export default function notification(
  message: string,
  type: 'info' | 'success' | 'warning' | 'error' = 'info'
) {
  return toast[type](message, {
    position: 'bottom-right',
    autoClose: 3000,
    hideProgressBar: false,
    closeOnClick: true,
    pauseOnHover: true,
    draggable: true,
  });
}
```

# 4. API Request Functions

With the Axios instance and notification utility in place, we can create functions for various types of HTTP requests, including GET, POST, PUT, and DELETE. These functions handle requests, responses, and errors consistently across your application.

## Here's an definition of the GET request function:

```ts
import { AxiosRequestConfig } from 'axios';

/**
 * Makes a GET request to the specified URL and returns the response data.
 * @param url - The URL to make the GET request to.
 * @param config - Optional Axios request configuration.
 * @returns A Promise that resolves to the response data.
 * @throws An error if the request fails.
 */
export const get = async <TResponse>(
  url: string,
  config?: AxiosRequestConfig
): Promise<TResponse> => {
  try {
    const response = await axiosInstance.get<TResponse>(url, config);

    return response.data;
  } catch (error) {
    const message = (error as AxiosError<{ message: string }>).response?.data
      ?.message;

    notification(`Error while fetching ${url}. ${message ?? ''}`, 'error');

    throw error;
  }
};
```

## Here's an definition of the POST request function:

```ts
import { AxiosRequestConfig } from 'axios';

/**
 * Sends a POST request to the specified URL with the given data and configuration.
 * @template TRequest The type of the request data.
 * @template TResponse The type of the response data.
 * @param {string} url The URL to send the request to.
 * @param {TRequest} data The data to send with the request.
 * @param {AxiosRequestConfig} [config] The configuration for the request.
 * @returns {Promise<TResponse>} A promise that resolves with the response data.
 * @throws {AxiosError} If the request fails.
 */
export const post = async <TRequest, TResponse>(
  url: string,
  data: TRequest,
  config?: AxiosRequestConfig
): Promise<TResponse> => {
  try {
    const response = await axiosInstance.post<TResponse>(url, data, config);

    return response.data;
  } catch (error) {
    const message = (error as AxiosError<{ message: string }>).response?.data
      ?.message;

    notification(`Error while posting ${url}. ${message ?? ''}`, 'error');

    throw error;
  }
};
```

## Here's an definition of the PUT request function:

```ts
import { AxiosRequestConfig } from 'axios';

/**
 * Sends a PUT request to the specified URL with the provided data and configuration.
 * @template TRequest The type of the request data.
 * @template TResponse The type of the response data.
 * @param {string} url The URL to send the request to.
 * @param {TRequest} data The data to send with the request.
 * @param {AxiosRequestConfig} [config] The configuration for the request.
 * @returns {Promise<TResponse>} A promise that resolves with the response data.
 * @throws {AxiosError} If the request fails.
 */
export const axiosPut = async <TRequest, TResponse>(
  url: string,
  data: TRequest,
  config?: AxiosRequestConfig
): Promise<TResponse> => {
  try {
    const response = await axiosInstance.put<TResponse>(url, data, config);

    return response.data;
  } catch (error) {
    const message = (error as AxiosError<{ message: string }>).response?.data
      ?.message;

    notification(`Error while updating ${url}. ${message ?? ''}`, 'error');
    throw error;
  }
};
```

## Here's an definition of the DELETE request function:

```ts
/**
 * Sends a DELETE request to the specified URL using axiosInstance.
 * @template TResponse The expected response type.
 * @param {string} url The URL to send the request to.
 * @param {AxiosRequestConfig} [config] The optional request configuration.
 * @returns {Promise<TResponse>} A promise that resolves with the response data.
 * @throws {AxiosError} If the request fails.
 */
export const axiosDelete = async <TResponse>(
  url: string,
  config?: AxiosRequestConfig
): Promise<TResponse> => {
  try {
    const response = await axiosInstance.delete<TResponse>(url, config);

    return response.data;
  } catch (error) {
    const message = (error as AxiosError<{ message: string }>).response?.data
      ?.message;

    notification(`Error while deleting ${url}. ${message ?? ''}`, 'error');

    throw error;
  }
};
```

# 5. Usage of the API Service

Now that we've created our API service, we can use it in our web applications. For example, we can use the `get` function to fetch a list of users from the API. If the request fails, the error will be logged and a notification will be displayed. First set the `VITE_BASE_SERVICE_URL` env variables for `BASE_URL` like this :

```env
VITE_BASE_SERVICE_URL=https://jsonplaceholder.typicode.com
```

Here is a example of `todos` with custom `useTodos` hook :

```ts
import { useCallback, useEffect, useState } from 'react';
import notification from '@lib/notification';
import { get } from '@services/api';

type Todo = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

/**
 * @description A hook for managing todos
 * @returns Returns the all functionality for todos
 */
export default function useTodos() {
  const [todos, setTodos] = useState<Todo[]>([]);

  const [loading, setLoading] = useState(false);

  const toggleTodo = (id: number) => () => {
    const newTodos = todos.map((todo) => {
      if (todo.id === id) {
        return { ...todo, completed: !todo.completed };
      }

      return todo;
    });

    notification('Todo status updated successfully.');
    setTodos(newTodos);
  };

  const updateTodoTitle = (id: number, title: string) => {
    const newTodos = todos.map((todo) => {
      if (todo.id === id) {
        return { ...todo, title };
      }

      return todo;
    });

    setTodos(newTodos);
  };

  const removeTodo = (id: number) => () => {
    const newTodos = todos.filter((todo) => todo.id !== id);

    notification(`Todo ${id} removed successfully.`, 'success');
    setTodos(newTodos);
  };

  const refetch = useCallback(async () => {
    setLoading(true);
    const list = await get<Todo[]>('todos');

    if (!list.length) {
      notification('Todo list is empty.', 'warning');
      setLoading(false);

      return;
    }

    notification(
      `Todo list loaded successfully. ${list.length} items found.`,
      'success'
    );

    setTodos(list);
    setLoading(false);
  }, []);

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

  return {
    todos,
    loading,
    refetch,
    toggleTodo,
    removeTodo,
    updateTodoTitle,
  };
}
```

Usage of this hook in your component like this :

```tsx
import { Button } from '@components/ui/button';
import useTodos from '@hooks/useTodos';
import { Checkbox } from '@components/ui/checkbox';
import { Input } from '@components/ui/input';
import { AiFillDelete } from 'react-icons/ai';

export default function Home() {
  const { loading, todos, toggleTodo, refetch, removeTodo, updateTodoTitle } =
    useTodos();

  if (loading) {
    return <div>Loading...</div>;
  }

  return (
    <div className="flex flex-col p-4 space-y-4">
      <h1 className="text-2xl font-bold text-center">Todos ({todos.length})</h1>
      <ul className="mt-4 grid lg:grid-cols-3 md:grid-cols-2 gap-2 h-[50vh] overflow-y-scroll border p-2 rounded-lg shadow-sm">
        {todos.map((todo) => (
          <li
            className="flex flex-row items-center justify-center gap-x-4"
            key={todo.id}
          >
            <Input
              onChange={(e) => updateTodoTitle(todo.id, e.target.value)}
              type="text"
              value={todo.title}
            />
            <Checkbox
              checked={todo.completed}
              onCheckedChange={toggleTodo(todo.id)}
              value={todo.id}
            />
            <AiFillDelete
              className="text-red-500 cursor-pointer w-7 h-7"
              onClick={removeTodo(todo.id)}
            />
          </li>
        ))}
      </ul>
      <Button onClick={refetch}>Refetch</Button>
    </div>
  );
}
```

# 6. Conclusion

In the ever-evolving landscape of web development, creating dependable APIs is essential for delivering seamless user experiences. Our exploration of a structured Axios API service demonstrates how to combine powerful error handling and user-friendly notifications to enhance the resilience of your web applications.

By following the steps outlined in this article, you can establish a solid foundation for your API service. Here's a quick recap of the key takeaways:

**Axios Configuration**: Create a dedicated Axios instance to manage global settings and interceptors. This approach ensures consistency and reusability throughout your application.

**Error Handling**: Implement error handling using Axios interceptors. This allows you to capture and manage errors efficiently, even in cases where requests are canceled.

**Notifications**: Employ a notification utility, such as `react-toastify`, to provide users with clear and informative feedback. This user-friendly touch can significantly improve the overall experience of your application.

**API Request Functions**: Utilize pre-defined functions for various HTTP request types **(GET, POST, PUT, DELETE)**. These functions handle requests, responses, and errors consistently, reducing redundancy and enhancing maintainability.

By integrating this structured Axios API service into your web applications, you can elevate their reliability and user-friendliness. The provided code serves as a versatile template that you can easily adapt and expand to meet your project's specific requirements.

Building resilient APIs is a fundamental aspect of modern web development, and with the techniques covered in this article, you're well-equipped to provide a more robust and user-friendly experience for your application's users.