Prerequisites
- Understanding of HTTP protocols and REST APIs 📝
- Experience with async/await and Promise patterns ⚡
- Familiarity with TypeScript generics and interfaces 💻
What you'll learn
- Master Axios for advanced HTTP communication patterns 🎯
- Implement type-safe request/response interceptors and transformers 🏗️
- Build resilient HTTP clients with automatic retries and caching 🐛
- Create enterprise-ready API abstractions and monitoring ✨
🎯 Introduction
Welcome to the powerful world of Axios! 🚀 If the Fetch API is like a reliable sedan, then Axios is the luxury SUV of HTTP clients - feature-packed, battle-tested, and ready for enterprise-scale applications!
Axios brings powerful features like automatic request/response transformations, interceptors, request cancellation, built-in error handling, and extensive configuration options. Combined with TypeScript’s type safety, you’ll build robust, maintainable HTTP clients that can handle anything from simple API calls to complex enterprise integrations.
By the end of this tutorial, you’ll be an Axios expert, capable of building production-ready HTTP infrastructure that scales with your application’s needs. Let’s dive into the advanced world of HTTP communication! 🌊
📚 Understanding Axios
🤔 What Makes Axios Special?
Axios is a Promise-based HTTP library that provides a rich feature set for making HTTP requests. It’s built on top of the browser’s XMLHttpRequest and Node.js’s http module, offering a consistent API across platforms.
// 🌟 Basic Axios setup with TypeScript
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
// 📦 Type definitions
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'moderator';
isActive: boolean;
createdAt: string;
updatedAt: string;
}
interface ApiResponse<T> {
data: T;
message: string;
status: 'success' | 'error';
timestamp: string;
}
// 🎯 Basic Axios instance with TypeScript
const api: AxiosInstance = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
// 🚀 Simple GET request with type safety
const getUser = async (id: number): Promise<User> => {
console.log(`📡 Fetching user ${id}...`);
const response: AxiosResponse<ApiResponse<User>> = await api.get(`/users/${id}`);
console.log('✅ User fetched successfully:', response.data.data);
return response.data.data;
};
// 📤 POST request with typed payload
const createUser = async (userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> => {
console.log('👤 Creating new user...');
const response: AxiosResponse<ApiResponse<User>> = await api.post('/users', userData);
console.log('✅ User created successfully:', response.data.data);
return response.data.data;
};
// 🎮 Usage example
const exampleUsage = async (): Promise<void> => {
try {
// 🔍 Fetch user
const user = await getUser(123);
// 👤 Create new user
const newUser = await createUser({
name: 'Jane Doe',
email: '[email protected]',
role: 'user',
isActive: true,
});
console.log('🎉 All operations completed successfully!');
} catch (error) {
console.error('💥 Operation failed:', error);
}
};
💡 Key Advantages Over Fetch
- 🔧 Rich Configuration: Extensive options for timeouts, headers, and transformations
- 🛡️ Built-in Error Handling: Automatic error throwing for HTTP error status codes
- 🔄 Request/Response Interceptors: Middleware pattern for cross-cutting concerns
- 📦 Request/Response Transformation: Automatic JSON parsing and data transformation
- 🚫 Request Cancellation: Built-in support for cancelling requests
- 📊 Upload Progress: Monitor file upload progress out of the box
// 🎨 Advanced Axios configuration
interface ApiClientConfig {
baseURL: string;
timeout?: number;
retries?: number;
retryDelay?: number;
enableLogging?: boolean;
authToken?: string;
}
class TypeSafeAxiosClient {
private instance: AxiosInstance;
private config: ApiClientConfig;
constructor(config: ApiClientConfig) {
this.config = {
timeout: 10000,
retries: 3,
retryDelay: 1000,
enableLogging: true,
...config,
};
this.instance = axios.create({
baseURL: this.config.baseURL,
timeout: this.config.timeout,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...(this.config.authToken && {
'Authorization': `Bearer ${this.config.authToken}`,
}),
},
});
this.setupInterceptors();
}
private setupInterceptors(): void {
// 📊 Request interceptor for logging and auth
this.instance.interceptors.request.use(
(config) => {
if (this.config.enableLogging) {
console.log(`🚀 ${config.method?.toUpperCase()} ${config.url}`, {
params: config.params,
data: config.data,
});
}
// 🔑 Add request timestamp for timing
config.metadata = { startTime: Date.now() };
return config;
},
(error) => {
console.error('💥 Request interceptor error:', error);
return Promise.reject(error);
}
);
// 📥 Response interceptor for logging and error handling
this.instance.interceptors.response.use(
(response) => {
if (this.config.enableLogging) {
const duration = Date.now() - response.config.metadata?.startTime;
console.log(`✅ ${response.config.method?.toUpperCase()} ${response.config.url} (${duration}ms)`, {
status: response.status,
data: response.data,
});
}
return response;
},
async (error) => {
const originalRequest = error.config;
// 🔄 Automatic retry logic
if (this.shouldRetry(error) && !originalRequest._retry) {
originalRequest._retry = true;
originalRequest._retryCount = (originalRequest._retryCount || 0) + 1;
if (originalRequest._retryCount <= this.config.retries!) {
console.log(`🔄 Retrying request (${originalRequest._retryCount}/${this.config.retries})...`);
await this.delay(this.config.retryDelay! * originalRequest._retryCount);
return this.instance(originalRequest);
}
}
// 🚨 Enhanced error logging
if (this.config.enableLogging) {
console.error(`❌ ${error.config?.method?.toUpperCase()} ${error.config?.url}`, {
status: error.response?.status,
message: error.message,
data: error.response?.data,
});
}
return Promise.reject(this.enhanceError(error));
}
);
}
private shouldRetry(error: any): boolean {
// 🔍 Retry on network errors or 5xx server errors
return (
!error.response || // Network error
error.response.status >= 500 || // Server error
error.response.status === 429 // Rate limited
);
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
private enhanceError(error: any): Error {
const enhancedError = new Error(error.message);
enhancedError.name = 'AxiosError';
(enhancedError as any).status = error.response?.status;
(enhancedError as any).data = error.response?.data;
(enhancedError as any).config = error.config;
return enhancedError;
}
// 🎯 Type-safe HTTP methods
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.get<T>(url, config);
return response.data;
}
async post<T, D = any>(url: string, data?: D, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.post<T>(url, data, config);
return response.data;
}
async put<T, D = any>(url: string, data?: D, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.put<T>(url, data, config);
return response.data;
}
async patch<T, D = any>(url: string, data?: D, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.patch<T>(url, data, config);
return response.data;
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.delete<T>(url, config);
return response.data;
}
// 🔧 Utility methods
setAuthToken(token: string): void {
this.instance.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
removeAuthToken(): void {
delete this.instance.defaults.headers.common['Authorization'];
}
getInstance(): AxiosInstance {
return this.instance;
}
}
🛠️ Advanced Request Configuration
🔧 Request Transformers and Interceptors
Axios provides powerful mechanisms to transform requests and responses before they’re processed:
// 🎛️ Advanced request/response transformation
interface RequestMetrics {
startTime: number;
endTime?: number;
duration?: number;
retryCount: number;
endpoint: string;
method: string;
}
interface ApiError {
code: string;
message: string;
details?: any;
timestamp: string;
requestId?: string;
}
class EnterpriseAxiosClient extends TypeSafeAxiosClient {
private requestMetrics: Map<string, RequestMetrics> = new Map();
constructor(config: ApiClientConfig) {
super(config);
this.setupAdvancedInterceptors();
}
private setupAdvancedInterceptors(): void {
const instance = this.getInstance();
// 📊 Advanced request interceptor
instance.interceptors.request.use(
(config) => {
// 🎯 Generate unique request ID
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
config.headers['X-Request-ID'] = requestId;
// 📈 Track request metrics
const metrics: RequestMetrics = {
startTime: Date.now(),
retryCount: 0,
endpoint: config.url || '',
method: config.method?.toUpperCase() || 'GET',
};
this.requestMetrics.set(requestId, metrics);
// 🔄 Request transformation
if (config.data && typeof config.data === 'object') {
// 📝 Add metadata to all requests
config.data = {
...config.data,
_metadata: {
clientVersion: '1.0.0',
timestamp: new Date().toISOString(),
requestId,
},
};
}
// 🔑 Dynamic authentication
const token = this.getStoredToken();
if (token && !config.headers['Authorization']) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
console.error('💥 Request preparation failed:', error);
return Promise.reject(error);
}
);
// 📥 Advanced response interceptor
instance.interceptors.response.use(
(response) => {
const requestId = response.config.headers['X-Request-ID'] as string;
const metrics = this.requestMetrics.get(requestId);
if (metrics) {
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
console.log(`📊 Request metrics:`, {
requestId,
endpoint: metrics.endpoint,
method: metrics.method,
duration: `${metrics.duration}ms`,
status: response.status,
});
// 🧹 Cleanup old metrics
this.requestMetrics.delete(requestId);
}
// 🎯 Response transformation
if (response.data && typeof response.data === 'object') {
// 📦 Extract nested data if following API response pattern
if (response.data.data !== undefined) {
response.data = response.data.data;
}
}
return response;
},
async (error) => {
const requestId = error.config?.headers['X-Request-ID'] as string;
const metrics = this.requestMetrics.get(requestId);
if (metrics) {
metrics.retryCount++;
}
// 🔄 Token refresh logic
if (error.response?.status === 401 && !error.config._tokenRefreshAttempted) {
try {
await this.refreshToken();
error.config._tokenRefreshAttempted = true;
return instance(error.config);
} catch (refreshError) {
console.error('🔑 Token refresh failed:', refreshError);
this.handleAuthFailure();
}
}
// 🚨 Enhanced error object
const enhancedError: ApiError = {
code: error.response?.data?.code || 'UNKNOWN_ERROR',
message: error.response?.data?.message || error.message,
details: error.response?.data?.details,
timestamp: new Date().toISOString(),
requestId,
};
throw enhancedError;
}
);
}
private getStoredToken(): string | null {
// Implementation depends on your storage strategy
return localStorage.getItem('authToken');
}
private async refreshToken(): Promise<void> {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await axios.post('/auth/refresh', {
refreshToken,
});
const { accessToken, refreshToken: newRefreshToken } = response.data;
localStorage.setItem('authToken', accessToken);
localStorage.setItem('refreshToken', newRefreshToken);
this.setAuthToken(accessToken);
}
private handleAuthFailure(): void {
// Clear stored tokens
localStorage.removeItem('authToken');
localStorage.removeItem('refreshToken');
// Redirect to login or emit auth failure event
window.location.href = '/login';
}
}
// 🎨 Custom request transformer
const customRequestTransformer = (data: any, headers: any) => {
// 🔄 Transform data before sending
if (data && typeof data === 'object') {
// 📅 Convert dates to ISO strings
Object.keys(data).forEach(key => {
if (data[key] instanceof Date) {
data[key] = data[key].toISOString();
}
});
// 🗜️ Compress large payloads
if (JSON.stringify(data).length > 10000) {
headers['Content-Encoding'] = 'gzip';
// Add compression logic here
}
}
return JSON.stringify(data);
};
// 📦 Custom response transformer
const customResponseTransformer = (data: any) => {
try {
const parsed = JSON.parse(data);
// 📅 Convert ISO strings back to dates
const dateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/;
function transformDates(obj: any): any {
if (obj === null || obj === undefined) return obj;
if (typeof obj === 'string' && dateRegex.test(obj)) {
return new Date(obj);
}
if (Array.isArray(obj)) {
return obj.map(transformDates);
}
if (typeof obj === 'object') {
const transformed: any = {};
Object.keys(obj).forEach(key => {
transformed[key] = transformDates(obj[key]);
});
return transformed;
}
return obj;
}
return transformDates(parsed);
} catch (error) {
return data;
}
};
// 🚀 Usage with custom transformers
const apiWithTransformers = new EnterpriseAxiosClient({
baseURL: 'https://api.example.com',
enableLogging: true,
});
const instance = apiWithTransformers.getInstance();
instance.defaults.transformRequest = [customRequestTransformer];
instance.defaults.transformResponse = [customResponseTransformer];
🚫 Request Cancellation and Timeouts
Handle request cancellation gracefully for better user experience:
// 🛑 Advanced request cancellation patterns
interface CancellableRequest<T> {
promise: Promise<T>;
cancel: () => void;
isCancelled: boolean;
}
class CancellableAxiosClient extends EnterpriseAxiosClient {
private activeCancelTokens: Map<string, AbortController> = new Map();
// 🎯 Create cancellable request
createCancellableRequest<T>(
requestFn: (signal: AbortSignal) => Promise<T>,
requestId?: string
): CancellableRequest<T> {
const id = requestId || `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const controller = new AbortController();
this.activeCancelTokens.set(id, controller);
const promise = requestFn(controller.signal)
.finally(() => {
this.activeCancelTokens.delete(id);
});
return {
promise,
cancel: () => {
controller.abort();
this.activeCancelTokens.delete(id);
},
get isCancelled() {
return controller.signal.aborted;
},
};
}
// 📊 Cancellable requests with timeout and retry
async getCancellable<T>(
url: string,
options: {
timeout?: number;
retries?: number;
requestId?: string;
onProgress?: (progress: number) => void;
} = {}
): CancellableRequest<T> {
const { timeout = 10000, retries = 3, requestId, onProgress } = options;
return this.createCancellableRequest(async (signal) => {
let attempt = 0;
let lastError: any;
while (attempt <= retries) {
try {
console.log(`🔄 Attempt ${attempt + 1}/${retries + 1} for ${url}`);
if (onProgress) {
onProgress((attempt / (retries + 1)) * 100);
}
const response = await this.getInstance().get<T>(url, {
signal,
timeout,
onDownloadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const progress = (progressEvent.loaded / progressEvent.total) * 100;
onProgress(progress);
}
},
});
if (onProgress) {
onProgress(100);
}
return response.data;
} catch (error: any) {
lastError = error;
if (signal.aborted) {
throw new Error('Request was cancelled');
}
if (attempt === retries || !this.shouldRetryError(error)) {
break;
}
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
console.log(`⏱️ Retrying in ${delay}ms...`);
await this.delay(delay);
attempt++;
}
}
throw lastError;
}, requestId);
}
// 📤 Cancellable file upload with progress
uploadFileCancellable(
url: string,
file: File,
options: {
onProgress?: (progress: number) => void;
onCancel?: () => void;
requestId?: string;
} = {}
): CancellableRequest<any> {
const { onProgress, onCancel, requestId } = options;
return this.createCancellableRequest(async (signal) => {
const formData = new FormData();
formData.append('file', file);
signal.addEventListener('abort', () => {
console.log('📁 File upload cancelled');
onCancel?.();
});
const response = await this.getInstance().post(url, formData, {
signal,
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const progress = (progressEvent.loaded / progressEvent.total) * 100;
onProgress(Math.round(progress));
}
},
});
return response.data;
}, requestId);
}
// 🧹 Cancel all active requests
cancelAllRequests(): void {
console.log(`🛑 Cancelling ${this.activeCancelTokens.size} active requests`);
this.activeCancelTokens.forEach((controller, requestId) => {
console.log(`❌ Cancelling request: ${requestId}`);
controller.abort();
});
this.activeCancelTokens.clear();
}
// 🔍 Get active request count
getActiveRequestCount(): number {
return this.activeCancelTokens.size;
}
private shouldRetryError(error: any): boolean {
return (
!error.response || // Network error
error.response.status >= 500 || // Server error
error.response.status === 429 // Rate limited
);
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 🎮 Usage examples
const cancellableClient = new CancellableAxiosClient({
baseURL: 'https://api.example.com',
enableLogging: true,
});
// 📊 Cancellable data fetching
const fetchUserData = async (userId: number): Promise<void> => {
const request = cancellableClient.getCancellable<User>(`/users/${userId}`, {
timeout: 5000,
retries: 2,
onProgress: (progress) => {
console.log(`📊 Progress: ${progress}%`);
},
});
// 🛑 Cancel after 3 seconds if needed
setTimeout(() => {
if (!request.isCancelled) {
console.log('⏰ Cancelling request due to timeout');
request.cancel();
}
}, 3000);
try {
const user = await request.promise;
console.log('✅ User fetched:', user);
} catch (error) {
console.error('❌ Failed to fetch user:', error);
}
};
// 📁 Cancellable file upload
const uploadFile = async (file: File): Promise<void> => {
const upload = cancellableClient.uploadFileCancellable('/upload', file, {
onProgress: (progress) => {
console.log(`📁 Upload progress: ${progress}%`);
// Update UI progress bar here
},
onCancel: () => {
console.log('🛑 Upload cancelled by user');
},
});
// 🎛️ User can cancel upload
document.getElementById('cancelBtn')?.addEventListener('click', () => {
upload.cancel();
});
try {
const result = await upload.promise;
console.log('✅ File uploaded successfully:', result);
} catch (error) {
console.error('❌ Upload failed:', error);
}
};
🎯 Type-Safe API Client Architecture
🏗️ Resource-Based Client Pattern
Create a scalable architecture for managing different API resources:
// 🎨 Generic resource client with full CRUD operations
interface ResourceClient<T, TCreate = Omit<T, 'id'>, TUpdate = Partial<TCreate>> {
getAll(params?: Record<string, any>): Promise<T[]>;
getById(id: string | number): Promise<T>;
create(data: TCreate): Promise<T>;
update(id: string | number, data: TUpdate): Promise<T>;
delete(id: string | number): Promise<void>;
search(query: string, filters?: Record<string, any>): Promise<T[]>;
}
abstract class BaseResourceClient<T, TCreate = Omit<T, 'id'>, TUpdate = Partial<TCreate>>
implements ResourceClient<T, TCreate, TUpdate> {
constructor(
protected client: CancellableAxiosClient,
protected endpoint: string
) {}
async getAll(params: Record<string, any> = {}): Promise<T[]> {
console.log(`📋 Fetching all ${this.endpoint}...`);
const response = await this.client.get<{ items: T[]; total: number }>(
this.endpoint,
{ params }
);
return response.items;
}
async getById(id: string | number): Promise<T> {
console.log(`🔍 Fetching ${this.endpoint}/${id}...`);
return this.client.get<T>(`${this.endpoint}/${id}`);
}
async create(data: TCreate): Promise<T> {
console.log(`✨ Creating new ${this.endpoint}...`);
return this.client.post<T>(this.endpoint, data);
}
async update(id: string | number, data: TUpdate): Promise<T> {
console.log(`🔄 Updating ${this.endpoint}/${id}...`);
return this.client.patch<T>(`${this.endpoint}/${id}`, data);
}
async delete(id: string | number): Promise<void> {
console.log(`🗑️ Deleting ${this.endpoint}/${id}...`);
await this.client.delete(`${this.endpoint}/${id}`);
}
async search(query: string, filters: Record<string, any> = {}): Promise<T[]> {
console.log(`🔍 Searching ${this.endpoint}: "${query}"`);
const params = { q: query, ...filters };
const response = await this.client.get<{ items: T[] }>(`${this.endpoint}/search`, {
params,
});
return response.items;
}
// 🛑 Create cancellable operations
protected createCancellableOperation<TResult>(
operation: (signal: AbortSignal) => Promise<TResult>,
requestId?: string
) {
return this.client.createCancellableRequest(operation, requestId);
}
}
// 👤 User resource client
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'moderator';
isActive: boolean;
lastLogin?: string;
createdAt: string;
updatedAt: string;
}
interface CreateUser {
name: string;
email: string;
password: string;
role?: 'user' | 'moderator';
}
interface UpdateUser {
name?: string;
email?: string;
role?: 'admin' | 'user' | 'moderator';
isActive?: boolean;
}
class UserClient extends BaseResourceClient<User, CreateUser, UpdateUser> {
constructor(client: CancellableAxiosClient) {
super(client, '/users');
}
// 🔍 User-specific methods
async getByEmail(email: string): Promise<User | null> {
try {
return await this.client.get<User>(`/users/by-email/${encodeURIComponent(email)}`);
} catch (error: any) {
if (error.status === 404) {
return null;
}
throw error;
}
}
async changePassword(id: number, currentPassword: string, newPassword: string): Promise<void> {
console.log(`🔐 Changing password for user ${id}...`);
await this.client.post(`/users/${id}/change-password`, {
currentPassword,
newPassword,
});
}
async toggleStatus(id: number): Promise<User> {
console.log(`🔄 Toggling status for user ${id}...`);
return this.client.post<User>(`/users/${id}/toggle-status`);
}
async getUserStats(id: number): Promise<{
loginCount: number;
lastActive: string;
createdPosts: number;
}> {
console.log(`📊 Fetching stats for user ${id}...`);
return this.client.get(`/users/${id}/stats`);
}
// 🛑 Cancellable user search with real-time updates
searchUsersLive(
query: string,
onUpdate: (users: User[]) => void,
requestId?: string
) {
return this.createCancellableOperation(async (signal) => {
const eventSource = new EventSource(`/users/search/live?q=${encodeURIComponent(query)}`);
signal.addEventListener('abort', () => {
eventSource.close();
});
return new Promise<User[]>((resolve, reject) => {
eventSource.onmessage = (event) => {
try {
const users: User[] = JSON.parse(event.data);
onUpdate(users);
} catch (error) {
reject(error);
}
};
eventSource.onerror = (error) => {
eventSource.close();
reject(error);
};
// Complete after 30 seconds
setTimeout(() => {
eventSource.close();
resolve([]);
}, 30000);
});
}, requestId);
}
}
// 📝 Post resource client
interface Post {
id: number;
title: string;
content: string;
excerpt: string;
authorId: number;
author?: User;
tags: string[];
published: boolean;
publishedAt?: string;
createdAt: string;
updatedAt: string;
}
interface CreatePost {
title: string;
content: string;
authorId: number;
tags?: string[];
published?: boolean;
}
class PostClient extends BaseResourceClient<Post, CreatePost> {
constructor(client: CancellableAxiosClient) {
super(client, '/posts');
}
async getByAuthor(authorId: number, includeUnpublished = false): Promise<Post[]> {
console.log(`📝 Fetching posts by author ${authorId}...`);
const params = { authorId, includeUnpublished };
return this.client.get<Post[]>('/posts/by-author', { params });
}
async publish(id: number): Promise<Post> {
console.log(`📢 Publishing post ${id}...`);
return this.client.post<Post>(`/posts/${id}/publish`);
}
async unpublish(id: number): Promise<Post> {
console.log(`📝 Unpublishing post ${id}...`);
return this.client.post<Post>(`/posts/${id}/unpublish`);
}
async getByTags(tags: string[]): Promise<Post[]> {
console.log(`🏷️ Fetching posts by tags: ${tags.join(', ')}`);
const params = { tags: tags.join(',') };
return this.client.get<Post[]>('/posts/by-tags', { params });
}
async getPopular(limit = 10): Promise<Post[]> {
console.log(`🔥 Fetching ${limit} popular posts...`);
return this.client.get<Post[]>(`/posts/popular?limit=${limit}`);
}
// 📊 Upload post images with progress
uploadImage(
postId: number,
file: File,
onProgress?: (progress: number) => void
) {
return this.client.uploadFileCancellable(`/posts/${postId}/images`, file, {
onProgress,
requestId: `upload_post_image_${postId}`,
});
}
}
// 🏗️ Main API service
class ApiService {
private client: CancellableAxiosClient;
public users: UserClient;
public posts: PostClient;
constructor(baseURL: string, authToken?: string) {
this.client = new CancellableAxiosClient({
baseURL,
authToken,
enableLogging: true,
retries: 3,
timeout: 15000,
});
this.setupGlobalInterceptors();
// 🎯 Initialize resource clients
this.users = new UserClient(this.client);
this.posts = new PostClient(this.client);
}
private setupGlobalInterceptors(): void {
const instance = this.client.getInstance();
// 📊 Global request metrics
instance.interceptors.request.use((config) => {
console.log(`🌐 API Request: ${config.method?.toUpperCase()} ${config.url}`);
return config;
});
// 🚨 Global error handling
instance.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 403) {
console.error('🚫 Access forbidden - insufficient permissions');
// Show user-friendly error message
} else if (error.response?.status >= 500) {
console.error('🔥 Server error - please try again later');
// Show server error message
}
return Promise.reject(error);
}
);
}
// 🔑 Authentication methods
async login(email: string, password: string): Promise<{ user: User; token: string }> {
console.log('🔐 Logging in...');
const response = await this.client.post<{ user: User; token: string }>('/auth/login', {
email,
password,
});
this.client.setAuthToken(response.token);
console.log('✅ Login successful');
return response;
}
async logout(): Promise<void> {
console.log('👋 Logging out...');
try {
await this.client.post('/auth/logout');
} catch (error) {
console.warn('⚠️ Logout request failed, but clearing local session');
}
this.client.removeAuthToken();
console.log('✅ Logout successful');
}
// 🛑 Utility methods
cancelAllRequests(): void {
this.client.cancelAllRequests();
}
getActiveRequestCount(): number {
return this.client.getActiveRequestCount();
}
// 📊 Health check
async healthCheck(): Promise<{ status: string; timestamp: string }> {
return this.client.get<{ status: string; timestamp: string }>('/health');
}
}
// 🚀 Usage example
const api = new ApiService('https://api.myapp.com');
// 🎮 Complete workflow example
const blogWorkflow = async (): Promise<void> => {
try {
// 🔐 Login
const { user, token } = await api.login('[email protected]', 'password123');
console.log(`👋 Welcome, ${user.name}!`);
// 👤 Get user stats
const stats = await api.users.getUserStats(user.id);
console.log('📊 User stats:', stats);
// 📝 Create a new post
const newPost = await api.posts.create({
title: 'My TypeScript Journey',
content: 'TypeScript has revolutionized my development workflow...',
authorId: user.id,
tags: ['typescript', 'programming', 'web-development'],
published: false,
});
// 📁 Upload an image for the post
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput.files && fileInput.files[0]) {
const imageUpload = api.posts.uploadImage(
newPost.id,
fileInput.files[0],
(progress) => {
console.log(`📊 Upload progress: ${progress}%`);
}
);
const uploadResult = await imageUpload.promise;
console.log('✅ Image uploaded:', uploadResult);
}
// 📢 Publish the post
const publishedPost = await api.posts.publish(newPost.id);
console.log('📢 Post published:', publishedPost);
console.log('🎉 Blog workflow completed successfully!');
} catch (error) {
console.error('💥 Workflow failed:', error);
}
};
🧪 Testing Axios Clients
🎭 Advanced Mocking and Testing Strategies
Create comprehensive tests for your Axios-based HTTP clients:
// 🧪 Advanced Axios testing utilities
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
interface TestScenario {
name: string;
setup: (mock: MockAdapter) => void;
execute: () => Promise<any>;
expect: (result: any, error?: any) => void;
}
class AxiosTestHelper {
private mock: MockAdapter;
private originalAxios: typeof axios;
constructor() {
this.mock = new MockAdapter(axios);
this.originalAxios = axios;
}
setup(): void {
this.mock.reset();
}
teardown(): void {
this.mock.restore();
}
// 🎭 Mock response patterns
mockSuccessResponse<T>(
method: 'get' | 'post' | 'put' | 'patch' | 'delete',
url: string | RegExp,
responseData: T,
status = 200,
headers = {}
): void {
this.mock[`on${method.charAt(0).toUpperCase() + method.slice(1)}`](url)
.reply(status, responseData, headers);
}
mockErrorResponse(
method: 'get' | 'post' | 'put' | 'patch' | 'delete',
url: string | RegExp,
status: number,
errorData?: any
): void {
this.mock[`on${method.charAt(0).toUpperCase() + method.slice(1)}`](url)
.reply(status, errorData);
}
mockNetworkError(
method: 'get' | 'post' | 'put' | 'patch' | 'delete',
url: string | RegExp
): void {
this.mock[`on${method.charAt(0).toUpperCase() + method.slice(1)}`](url)
.networkError();
}
mockTimeout(
method: 'get' | 'post' | 'put' | 'patch' | 'delete',
url: string | RegExp
): void {
this.mock[`on${method.charAt(0).toUpperCase() + method.slice(1)}`](url)
.timeout();
}
// 🔄 Mock delayed responses
mockDelayedResponse<T>(
method: 'get' | 'post' | 'put' | 'patch' | 'delete',
url: string | RegExp,
responseData: T,
delay: number,
status = 200
): void {
this.mock[`on${method.charAt(0).toUpperCase() + method.slice(1)}`](url)
.reply(() => {
return new Promise(resolve => {
setTimeout(() => {
resolve([status, responseData]);
}, delay);
});
});
}
// 📊 Get request history
getRequestHistory(): Array<{
method: string;
url: string;
data?: any;
params?: any;
}> {
return this.mock.history.get.concat(
this.mock.history.post,
this.mock.history.put,
this.mock.history.patch,
this.mock.history.delete
).map(req => ({
method: req.method?.toUpperCase() || 'UNKNOWN',
url: req.url || '',
data: req.data ? JSON.parse(req.data) : undefined,
params: req.params,
}));
}
// 🎯 Run test scenarios
async runScenarios(scenarios: TestScenario[]): Promise<void> {
for (const scenario of scenarios) {
console.log(`🧪 Running scenario: ${scenario.name}`);
this.mock.reset();
scenario.setup(this.mock);
try {
const result = await scenario.execute();
scenario.expect(result);
console.log(`✅ Scenario passed: ${scenario.name}`);
} catch (error) {
scenario.expect(null, error);
console.log(`✅ Scenario passed (expected error): ${scenario.name}`);
}
}
}
}
// 🧪 Comprehensive test suite
describe('ApiService', () => {
let testHelper: AxiosTestHelper;
let apiService: ApiService;
beforeEach(() => {
testHelper = new AxiosTestHelper();
testHelper.setup();
apiService = new ApiService('https://api.test.com');
});
afterEach(() => {
testHelper.teardown();
});
describe('User Operations', () => {
it('should fetch user by ID with proper error handling', async () => {
const mockUser: User = {
id: 1,
name: 'John Doe',
email: '[email protected]',
role: 'user',
isActive: true,
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
};
testHelper.mockSuccessResponse('get', '/users/1', mockUser);
const user = await apiService.users.getById(1);
expect(user).toEqual(mockUser);
const history = testHelper.getRequestHistory();
expect(history).toHaveLength(1);
expect(history[0].url).toBe('/users/1');
});
it('should handle user not found gracefully', async () => {
testHelper.mockErrorResponse('get', '/users/999', 404, {
code: 'USER_NOT_FOUND',
message: 'User not found',
});
await expect(apiService.users.getById(999)).rejects.toMatchObject({
code: 'USER_NOT_FOUND',
message: 'User not found',
});
});
it('should retry on server errors', async () => {
const mockUser: User = {
id: 1,
name: 'John Doe',
email: '[email protected]',
role: 'user',
isActive: true,
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
};
// First request fails with 500
testHelper.mockErrorResponse('get', '/users/1', 500);
// Second request succeeds
testHelper.mockSuccessResponse('get', '/users/1', mockUser);
const user = await apiService.users.getById(1);
expect(user).toEqual(mockUser);
const history = testHelper.getRequestHistory();
expect(history.length).toBeGreaterThan(1); // Should have retried
});
});
describe('Authentication Flow', () => {
it('should handle login and token management', async () => {
const mockLoginResponse = {
user: {
id: 1,
name: 'John Doe',
email: '[email protected]',
role: 'user' as const,
isActive: true,
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
},
token: 'mock-jwt-token',
};
testHelper.mockSuccessResponse('post', '/auth/login', mockLoginResponse);
const result = await apiService.login('[email protected]', 'password123');
expect(result).toEqual(mockLoginResponse);
const history = testHelper.getRequestHistory();
expect(history[0].data).toEqual({
email: '[email protected]',
password: 'password123',
});
});
it('should handle token refresh on 401 errors', async () => {
const mockUser: User = {
id: 1,
name: 'John Doe',
email: '[email protected]',
role: 'user',
isActive: true,
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
};
// First request returns 401
testHelper.mockErrorResponse('get', '/users/1', 401);
// Token refresh succeeds
testHelper.mockSuccessResponse('post', '/auth/refresh', {
accessToken: 'new-token',
refreshToken: 'new-refresh-token',
});
// Retry with new token succeeds
testHelper.mockSuccessResponse('get', '/users/1', mockUser);
// Mock localStorage
Object.defineProperty(window, 'localStorage', {
value: {
getItem: jest.fn().mockReturnValue('refresh-token'),
setItem: jest.fn(),
removeItem: jest.fn(),
},
writable: true,
});
const user = await apiService.users.getById(1);
expect(user).toEqual(mockUser);
});
});
describe('Request Cancellation', () => {
it('should cancel requests properly', async () => {
testHelper.mockDelayedResponse('get', '/users/1', { id: 1 }, 5000);
const cancellableClient = new CancellableAxiosClient({
baseURL: 'https://api.test.com',
});
const request = cancellableClient.getCancellable('/users/1');
// Cancel after 100ms
setTimeout(() => {
request.cancel();
}, 100);
await expect(request.promise).rejects.toThrow('Request was cancelled');
expect(request.isCancelled).toBe(true);
});
});
describe('Error Scenarios', () => {
const errorScenarios: TestScenario[] = [
{
name: 'Network Error',
setup: (mock) => {
testHelper.mockNetworkError('get', '/users/1');
},
execute: () => apiService.users.getById(1),
expect: (result, error) => {
expect(error).toBeDefined();
expect(error.message).toContain('Network Error');
},
},
{
name: 'Timeout Error',
setup: (mock) => {
testHelper.mockTimeout('get', '/users/1');
},
execute: () => apiService.users.getById(1),
expect: (result, error) => {
expect(error).toBeDefined();
expect(error.code).toBe('ECONNABORTED');
},
},
{
name: 'Server Error with Retry',
setup: (mock) => {
// Multiple 500 errors to test retry exhaustion
testHelper.mockErrorResponse('get', '/users/1', 500);
testHelper.mockErrorResponse('get', '/users/1', 500);
testHelper.mockErrorResponse('get', '/users/1', 500);
testHelper.mockErrorResponse('get', '/users/1', 500);
},
execute: () => apiService.users.getById(1),
expect: (result, error) => {
expect(error).toBeDefined();
expect(error.status).toBe(500);
},
},
];
it('should handle various error scenarios', async () => {
await testHelper.runScenarios(errorScenarios);
});
});
describe('Performance and Load Testing', () => {
it('should handle concurrent requests efficiently', async () => {
const mockUser: User = {
id: 1,
name: 'John Doe',
email: '[email protected]',
role: 'user',
isActive: true,
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
};
// Mock multiple user responses
for (let i = 1; i <= 10; i++) {
testHelper.mockSuccessResponse('get', `/users/${i}`, {
...mockUser,
id: i,
name: `User ${i}`,
});
}
const startTime = Date.now();
// Make 10 concurrent requests
const promises = Array.from({ length: 10 }, (_, i) =>
apiService.users.getById(i + 1)
);
const results = await Promise.all(promises);
const endTime = Date.now();
expect(results).toHaveLength(10);
expect(endTime - startTime).toBeLessThan(1000); // Should complete quickly
const history = testHelper.getRequestHistory();
expect(history).toHaveLength(10);
});
});
});
// 🎭 Integration testing utilities
class ApiIntegrationTest {
private apiService: ApiService;
private testData: {
users: User[];
posts: Post[];
} = {
users: [],
posts: [],
};
constructor(baseURL: string) {
this.apiService = new ApiService(baseURL);
}
async setupTestData(): Promise<void> {
console.log('🎭 Setting up test data...');
// Create test users
for (let i = 1; i <= 3; i++) {
const user = await this.apiService.users.create({
name: `Test User ${i}`,
email: `test${i}@example.com`,
password: 'testpass123',
role: i === 1 ? 'admin' : 'user',
});
this.testData.users.push(user);
}
// Create test posts
for (let i = 1; i <= 5; i++) {
const post = await this.apiService.posts.create({
title: `Test Post ${i}`,
content: `This is test post content ${i}`,
authorId: this.testData.users[i % 3].id,
tags: [`tag${i}`, 'test'],
published: i % 2 === 0,
});
this.testData.posts.push(post);
}
console.log('✅ Test data setup complete');
}
async runFullWorkflowTest(): Promise<void> {
console.log('🚀 Running full workflow integration test...');
try {
// 1. Login
const { user } = await this.apiService.login(
this.testData.users[0].email,
'testpass123'
);
console.log('✅ Login successful');
// 2. Fetch user stats
const stats = await this.apiService.users.getUserStats(user.id);
console.log('✅ User stats fetched:', stats);
// 3. Search posts
const searchResults = await this.apiService.posts.search('test');
console.log('✅ Post search completed:', searchResults.length);
// 4. Create new post
const newPost = await this.apiService.posts.create({
title: 'Integration Test Post',
content: 'This post was created during integration testing',
authorId: user.id,
tags: ['integration', 'test'],
published: false,
});
console.log('✅ New post created:', newPost.id);
// 5. Publish post
const publishedPost = await this.apiService.posts.publish(newPost.id);
console.log('✅ Post published:', publishedPost.published);
// 6. Get posts by author
const authorPosts = await this.apiService.posts.getByAuthor(user.id);
console.log('✅ Author posts fetched:', authorPosts.length);
// 7. Logout
await this.apiService.logout();
console.log('✅ Logout successful');
console.log('🎉 Full workflow integration test completed successfully!');
} catch (error) {
console.error('💥 Integration test failed:', error);
throw error;
}
}
async cleanup(): Promise<void> {
console.log('🧹 Cleaning up test data...');
try {
// Delete test posts
for (const post of this.testData.posts) {
await this.apiService.posts.delete(post.id);
}
// Delete test users
for (const user of this.testData.users) {
await this.apiService.users.delete(user.id);
}
console.log('✅ Test data cleanup complete');
} catch (error) {
console.warn('⚠️ Cleanup partially failed:', error);
}
}
}
// 🧪 Run integration tests
const runIntegrationTests = async (): Promise<void> => {
if (process.env.NODE_ENV === 'test' && process.env.API_BASE_URL) {
const integrationTest = new ApiIntegrationTest(process.env.API_BASE_URL);
try {
await integrationTest.setupTestData();
await integrationTest.runFullWorkflowTest();
} finally {
await integrationTest.cleanup();
}
}
};
🎯 Conclusion
Congratulations! 🎉 You’ve mastered Axios with TypeScript and built enterprise-grade HTTP clients! You now have the expertise to:
- 🚀 Master Axios features with type-safe request/response patterns and interceptors
- 🔧 Build resilient clients with automatic retries, cancellation, and error recovery
- 🏗️ Create scalable architectures with resource-based patterns and middleware
- 🧪 Test HTTP infrastructure with comprehensive mocking and integration strategies
Axios combined with TypeScript provides a powerful foundation for building robust, maintainable HTTP communication layers. From simple API calls to complex enterprise integrations, you’re now equipped to handle any HTTP challenge with confidence and style!
Keep exploring advanced patterns, experiment with GraphQL integration, and remember that great HTTP architecture is the backbone of excellent user experiences. The API world is yours to conquer! 🌐✨