Centralizing Your API Calls with Axios: TypeScript Example
L.

L. @jionnlol

About: writing code is an art - at least to me. just sharing some markdown files I keep at dev.to

Joined:
May 21, 2025

Centralizing Your API Calls with Axios: TypeScript Example

Publish Date: May 21
1 0

TL;DR: Learn how to create a centralized, robust, and reusable Axios client in TypeScript that handles encryption, error categorization, logging, and consistent response handling — all while improving maintainability across your app.


If you're building a medium-to-large-scale web application using TypeScript (especially with React or Node.js), you've probably run into this situation:

“Wait… why is each API call doing its own error handling and decrypting logic?!”

That’s where a centralized Axios instance becomes a game-changer. 🔍 In this post, I'll walk through how we implemented a clean, scalable AxiosClient class in our project — and why it's the secret sauce behind robust API communication.


🧱 The Problem: Disorganized API Layer

Before implementing our AxiosClient, every API file had its own way of:

  • Handling errors ❌
  • Decrypting payloads 🔐
  • Logging responses 📜
  • Managing timeouts or retry logic ⏳

This led to:

  • Code duplication 🐛
  • Inconsistent error messages 💣
  • Hard-to-debug flows 🤯

So we decided to unify everything under one roof — enter the AxiosClient. 🎯


🌟 What Is AxiosClient?

The AxiosClient is a singleton Axios instance configured with interceptors for response/error handling. It ensures:

  • One source of truth for all API calls 🔄
  • Consistent structure in success/failure cases ✅
  • Encrypted payload support 🔐
  • Debug-friendly logs 📦
  • Clear error categorization 🚨

Here’s what it does under the hood:


✅ Singleton Pattern

Only one instance is created and reused throughout the app. No more redundant config setups!

private static instance: AxiosInstance;

public static getInstance(): AxiosInstance {
  if (!AxiosClient.instance) {
    AxiosClient.instance = axios.create({
      baseURL: BACKEND_BASE_URL,
      timeout: 10000,
      headers: { "Content-Type": "application/json" },
    });
    AxiosClient.instance.interceptors.response.use(
      (response) => AxiosClient.handleApiResponse(response),
      (error) => AxiosClient.handleApiError(error)
    );
  }
  return AxiosClient.instance;
}
Enter fullscreen mode Exit fullscreen mode

🔁 Response Interception

Every successful response goes through handleApiResponse(). This method:

  • Checks for valid data structure 🧪
  • Handles encrypted payloads via CryptoHelper.decryptPayload 🔐
  • Ensures success: true before returning actual data ✅
  • Throws AppError when something goes wrong ❌
private static async handleApiResponse<T>(
  response: AxiosResponse<ResponseModel<T> | EncryptedMessage>
): Promise<AxiosResponse<T>> {
  ...
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Error Interception

All errors are caught by handleApiError(), which categorizes them:

  • API Errors: Based on HTTP status codes (e.g., 401 Unauthorized) 🚫
  • Network Errors: When no response is received 🌐
  • Unknown Errors: Everything else 🤷‍♂️

Each error is wrapped in an AppError class with metadata like message, code, and original response.

private static handleApiError(error: AxiosError | AppError): never {
  ...
}
Enter fullscreen mode Exit fullscreen mode

🔐 Encrypted Payloads Support

We use AES-GCM or similar algorithms to encrypt sensitive payloads on the backend. Our client automatically detects and decrypts these payloads using CryptoHelper.

if (typeof data.payload === "object" && "iv" in data.payload && "data" in data.payload) {
  data.payload = await CryptoHelper.decryptPayload(data.payload);
}
Enter fullscreen mode Exit fullscreen mode

If decryption fails, it throws a clear AppError.


📦 How to Use It in Your APIs

Once you have the AxiosClient, using it is simple:

import AxiosClient from "../classes/AxiosClient";

const apiClient = AxiosClient.getInstance();

export async function fetchUserData() {
  const response = await apiClient.get("/user/profile");
  return response.data; // Already decrypted and validated!
}
Enter fullscreen mode Exit fullscreen mode

No need to write error handling or decryption logic here — it's already taken care of. 🧼


🎯 Benefits of Using AxiosClient

Benefit Description
Centralized Configuration 🧩 All API settings live in one place
Consistent Behavior 🔄 Every API call follows the same flow
Improved Debugging 🔍 Logs help track down issues faster
Scalable Architecture 🚀 Easy to extend with new interceptors
Security Ready 🔐 Built-in support for encrypted payloads
Maintainable Codebase 🛠️ Changes propagate everywhere instantly

TL;DR Summary

Create a centralized Axios client to:

  • Reduce boilerplate 🧹
  • Handle common tasks (errors, logging, decryption) 🧰
  • Improve consistency 🔄
  • Boost developer productivity 💡

With just one class (AxiosClient), you get a powerful abstraction layer over your entire API surface. 🧠


📝 Missing Implementations You’ll Need

To make this work, here are the key pieces you’ll want to implement yourself:

1. CryptoHelper.ts

A helper class for decrypting payloads. You can use libraries like crypto-js or Web Crypto API.

export class CryptoHelper {
  static async decryptPayload(payload: EncryptedMessage): Promise<any> {
    // your AES-GCM or equivalent decryption logic here 🔐
  }
}
Enter fullscreen mode Exit fullscreen mode

2. AppError.ts

A custom error class that wraps extra context like error code, raw data, etc.

export class AppError extends Error {
  constructor(
    public message: string,
    public code?: string,
    public data?: any
  ) {
    super(message);
    this.name = "AppError";
  }
}
Enter fullscreen mode Exit fullscreen mode

3. constants.ts

Error messages and base URL constants.

export const AXIOS_CLIENT_ERROR_MESSAGES = {
  MISSING_RESPONSE_DATA: "Response data is missing",
  INVALID_RESPONSE_FORMAT: "Invalid response format",
  DECRYPTION_FAILED: "Failed to decrypt payload",
  FAILED_AT_API_LEVEL: "Request failed at API level",
  NO_RESPONSE_FROM_SERVER: "No response from server",
  UNSPECIFIED_ERROR: "An unspecified error occurred",
  MISSING_PAYLOAD: "Missing payload in response",
  INVALID_ENCRYPTED_MESSAGE: "Invalid encrypted message"
};

export const BACKEND_BASE_URL = process.env.BACKEND_BASE_URL || "https://api.yourapp.com";
Enter fullscreen mode Exit fullscreen mode

📦 Interfaces You Might Need

ResponseModel<T>

export interface ResponseModel<T> {
  success: boolean;
  payload: T;
}
Enter fullscreen mode Exit fullscreen mode

EncryptedMessage

export interface EncryptedMessage {
  iv: string;
  data: string;
}
Enter fullscreen mode Exit fullscreen mode

👨‍💻 Final Thoughts

Using a centralized Axios client isn’t just about reducing lines of code — it's about shaping your application architecture around reusability, readability, and robustness. 🧠

Whether you're working solo or in a team, investing time upfront to set up a solid Axios foundation will pay dividends later. 💸

Happy coding! 🚀

full code:

import axios, { AxiosError, AxiosInstance, AxiosResponse } from "axios";
import { AXIOS_CLIENT_ERROR_MESSAGES, BACKEND_BASE_URL } from "../constants";
import { CryptoHelper } from "../helpers/cryptoHelper";
import { AppError } from "./AppError";
import { EncryptedMessage, ResponseModel } from "../interfaces/transmitModel";

/**
 * 🚀 Centralized Axios instance that wraps all API calls.
 *
 * Features:
 * - Singleton pattern to reuse one instance
 * - Interceptors for consistent response/error handling
 * - Automatic decryption of encrypted payloads
 * - Unified error categorization (API, network, unknown)
 * - Custom logging (optional or replaceable)
 */
export default class AxiosClient {
  private static instance: AxiosInstance;

  /**
   * 🔄 Returns a singleton instance of Axios.
   * Only creates once and reuses in subsequent calls.
   */
  public static getInstance(): AxiosInstance {
    if (!AxiosClient.instance) {
      // Create a new Axios instance with base config
      AxiosClient.instance = axios.create({
        baseURL: BACKEND_BASE_URL,
        timeout: 10000, // 10 seconds timeout
        headers: {
          "Content-Type": "application/json",
        },
      });

      // Setup interceptors for responses
      AxiosClient.instance.interceptors.response.use(
        (response) => AxiosClient.handleApiResponse(response),
        (error) => AxiosClient.handleApiError(error)
      );
    }

    return AxiosClient.instance;
  }

  /**
   * 📤 Handles successful responses from the server.
   * - Validates structure
   * - Decrypts payloads if needed
   * - Throws AppError on failure
   */
  private static async handleApiResponse<T>(
    response: AxiosResponse<ResponseModel<T> | EncryptedMessage>
  ): Promise<AxiosResponse<T>> {
    try {
      // Log raw response for debugging (can be replaced with real logger)
      console.log("📡 API Response Received:", response);

      // Ensure we have data in the response
      if (!response.data) {
        throw new AppError(AXIOS_CLIENT_ERROR_MESSAGES.MISSING_RESPONSE_DATA);
      }

      const responseData = response.data;

      // Check if it's a standard success/failure response
      if ("success" in responseData && "payload" in responseData) {
        // Standard API response
        const data = responseData as ResponseModel<T>;

        if (!data.success) {
          // Handle known API-level errors
          let errorData: any = AXIOS_CLIENT_ERROR_MESSAGES.UNKNOWN_ERROR;

          if (data.payload && typeof data.payload === "object" && "iv" in data.payload && "data" in data.payload) {
            // If payload is encrypted, attempt to decrypt
            errorData = await CryptoHelper.decryptPayload(data.payload).catch(() =>
              AXIOS_CLIENT_ERROR_MESSAGES.DECRYPTION_FAILED
            );
          } else if (data.payload) {
            errorData = AXIOS_CLIENT_ERROR_MESSAGES.INVALID_ENCRYPTED_MESSAGE;
          }

          throw new AppError(AXIOS_CLIENT_ERROR_MESSAGES.FAILED_AT_API_LEVEL, undefined, errorData);
        }

        if (!data.payload) {
          throw new AppError(AXIOS_CLIENT_ERROR_MESSAGES.MISSING_PAYLOAD);
        }

        // If payload is encrypted, decrypt it
        if (
          typeof data.payload === "object" &&
          "iv" in data.payload &&
          "data" in data.payload
        ) {
          data.payload = (await CryptoHelper.decryptPayload(data.payload).catch(() => {
            throw new AppError(AXIOS_CLIENT_ERROR_MESSAGES.DECRYPTION_FAILED);
          })) as T;
        }

        // Return modified response with decrypted payload
        return { ...response, data: data.payload } as AxiosResponse<T>;
      }

      // If not a standard format, assume it's a direct encrypted message
      if ("iv" in responseData && "data" in responseData) {
        const decrypted = await CryptoHelper.decryptPayload(responseData as EncryptedMessage);
        return { ...response, data: decrypted } as AxiosResponse<T>;
      }

      // Unknown response format
      throw new AppError(AXIOS_CLIENT_ERROR_MESSAGES.INVALID_RESPONSE_FORMAT);
    } catch (err) {
      // Wrap generic errors into AppError
      throw err instanceof AppError ? err : new AppError(AXIOS_CLIENT_ERROR_MESSAGES.UNSPECIFIED_ERROR, undefined, err);
    }
  }

  /**
   * ⚠️ Handles Axios and general errors.
   * Categorizes them into:
   * - API Errors (4xx, 5xx)
   * - Network Errors
   * - Unknown Errors
   */
  private static handleApiError(error: AxiosError | AppError): never {
    if (error instanceof AppError) {
      // Already handled by us ✅
      throw error;
    }

    if (axios.isAxiosError(error)) {
      // Server responded with status code
      if (error.response) {
        const { status, data: responseData } = error.response;

        // Map common HTTP status codes to readable messages
        const defaultMessage =
          {
            400: "Bad Request",
            401: "Unauthorized",
            403: "Forbidden",
            404: "Not Found",
            409: "Conflict",
            429: "Too Many Requests",
            500: "Internal Server Error",
            503: "Service Unavailable",
          }[status] ||
          (responseData as { message?: string })?.message ||
          AXIOS_CLIENT_ERROR_MESSAGES.FAILED_AT_API_LEVEL;

        throw new AppError(defaultMessage, `${status}`, responseData);
      }

      // No response received (network issue)
      if (error.request) {
        throw new AppError(AXIOS_CLIENT_ERROR_MESSAGES.NO_RESPONSE_FROM_SERVER);
      }

      // Something else went wrong during request setup
      throw new AppError(AXIOS_CLIENT_ERROR_MESSAGES.UNSPECIFIED_ERROR, undefined, error.message);
    }

    // Unknown error type
    console.error("🚨 Unknown error:", error);
    throw new AppError(AXIOS_CLIENT_ERROR_MESSAGES.UNSPECIFIED_ERROR);
  }
}
Enter fullscreen mode Exit fullscreen mode

Comments 0 total

    Add comment