5 min read

Essential API Interaction Utilities for Web Projects

Introduction

Efficient API interaction is crucial for building responsive and data-driven applications in modern web development. This blog post explores a set of utility functions designed to simplify and streamline API calls in React TypeScript projects. We’ll cover functions for URL construction, GET and POST requests, and introduce error handling and request cancellation techniques.

The Core Utility Functions

Let’s examine the key functions that form the backbone of our API interaction toolkit:

1. buildURL: Constructing Dynamic URLs

import queryString from "query-string";

export function buildURL(
  url: string,
  params?: Record<string, any> | string
): string {
  if (!params) return url;

  const separator = url.includes("?") ? "&" : "?";
  const queryParams =
    typeof params === "object" ? queryString.stringify(params) : params;

  return `${url}${separator}${queryParams}`;
}

This function dynamically constructs URLs by appending query parameters. It’s versatile, handling both object and string parameters, making it invaluable for creating complex API endpoints.

2. fetchData: Simple GET Requests

import axios, { AxiosInstance } from "axios";

const axiosInstance: AxiosInstance = axios.create({
  baseURL: process.env.REACT_APP_API_BASE_URL,
});

export async function fetchData<T>(
  url: string,
  params?: Record<string, any> | string
): Promise<T> {
  try {
    const response = await axiosInstance.get<T>(buildURL(url, params));
    return response.data;
  } catch (error) {
    console.error("Error in fetchData:", error);
    throw error;
  }
}

fetchData provides a straightforward way to make GET requests. It utilizes the buildURL function and a configured Axios instance, simplifying basic data fetching operations.

3. fetchPaginatedData: Advanced GET Requests with Pagination

export async function fetchPaginatedData<T>({
  url,
  params = {},
}: {
  url: string;
  params?: { page?: number; pageSize?: number; [key: string]: any };
}): Promise<T> {
  const { page = 0, pageSize = 10, ...rest } = params;
  const fullUrl = buildURL(url, { page, pageSize, ...rest });

  try {
    const response = await axiosInstance.get<T>(fullUrl);
    return response.data;
  } catch (error) {
    console.error("Error in fetchPaginatedData:", error);
    throw error;
  }
}

fetchPaginatedData extends fetchData’s functionality by adding built-in support for pagination. It’s ideal for scenarios where you’re dealing with large datasets that require page-by-page loading.

4. postData: Handling POST Requests

export async function postData<T>({
  url,
  data,
}: {
  url: string;
  data?: Record<string, any> | string;
}): Promise<T> {
  try {
    const response = await axiosInstance.post<T>(url, data);
    return response.data;
  } catch (error) {
    console.error("Error in postData:", error);
    throw error;
  }
}

The postData function simplifies making POST requests, handling data submission to your API endpoints.

Enhanced Error Handling and Request Cancellation

To make our utility functions more robust, let’s add error handling and request cancellation capabilities:

5. createCancelToken: Generate Cancellation Tokens

import axios, { CancelTokenSource } from "axios";

export function createCancelToken(): CancelTokenSource {
  return axios.CancelToken.source();
}

This function creates a cancel token, which can be used to abort requests if needed.

6. withCancellation: Wrapper for Cancellable Requests

export async function withCancellation<T>(
  promise: Promise<T>,
  cancelToken: CancelTokenSource
): Promise<T> {
  try {
    return await promise;
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log("Request canceled:", error.message);
    }
    throw error;
  }
}

withCancellation wraps a promise with cancellation logic, allowing you to handle cancelled requests gracefully.

Putting It All Together

Here’s an example of how these functions can be used in a React TypeScript component:

import React, { useState, useEffect } from "react";
import {
  fetchPaginatedData,
  postData,
  createCancelToken,
  withCancellation,
} from "../utils/api";

interface User {
  id: number;
  name: string;
}

const UserList: React.FC = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [cancelToken, setCancelToken] = useState(createCancelToken());

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const data = await withCancellation(
          fetchPaginatedData<{ users: User[] }>({
            url: "/users",
            params: { page: 1, pageSize: 10 },
          }),
          cancelToken
        );
        setUsers(data.users);
      } catch (error) {
        console.error("Failed to fetch users:", error);
      }
    };

    fetchUsers();

    return () => {
      cancelToken.cancel("Component unmounted");
    };
  }, []);

  const addUser = async (userData: Omit<User, "id">) => {
    try {
      const newUser = await postData<User>({ url: "/users", data: userData });
      setUsers((prevUsers) => [...prevUsers, newUser]);
    } catch (error) {
      console.error("Failed to add user:", error);
    }
  };

  const cancelRequest = () => {
    cancelToken.cancel("Operation canceled by the user.");
    setCancelToken(createCancelToken()); // Create a new token for future requests
  };

  return (
    <div>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      <button onClick={cancelRequest}>Cancel Request</button>
    </div>
  );
};

export default UserList;

Conclusion

These utility functions provide a solid foundation for managing API interactions in your React TypeScript applications. By encapsulating common patterns and adding robust error handling and cancellation capabilities, you can significantly streamline your data fetching logic and improve the overall reliability of your application.