DEV Community

L.
L.

Posted on

Centralizing Your API Calls with Axios: TypeScript Example

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

Top comments (0)