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; }
🔁 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>> { ... }
⚠️ 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 { ... }
🔐 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); }
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! }
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 🔐 } }
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"; } }
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";
📦 Interfaces You Might Need
ResponseModel<T>
export interface ResponseModel<T> { success: boolean; payload: T; }
EncryptedMessage
export interface EncryptedMessage { iv: string; data: string; }
👨💻 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); } }
Top comments (0)