Axios Project — Build a Complete Application with Axios
In this tutorial, you'll learn to build a complete Axios project. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Build a complete Axios API client library that combines instances, interceptors, authentication, request queuing, retry logic, progress tracking, and Caching into a production-ready HTTP client.
What You'll Learn
By the end of this tutorial, you'll have built a full-featured API client with automatic retry, token management, request deduplication, progress tracking, and comprehensive error handling.
Why It Matters
Production API clients need more than basic GET and POST calls. They need retry logic for network failures, automatic token refresh, request deduplication, and graceful error handling that doesn't crash the app.
Real-World Use
The API client pattern in this project mirrors what Durga Antivirus Pro uses for its cloud threat database. The client handles millions of requests daily with automatic retries, cached responses, and seamless token management.
Where This Fits in Your Learning Path
flowchart LR
A["HTTP/2 Adapters"] --> B["**Axios Project**"]
B --> C["Production Axios Apps"]
style B fill:#f97316,stroke:#c2410c,color:#fff
style A fill:#e5e7eb,stroke:#9ca3af,color:#374151
style C fill:#22c55e,stroke:#16a34a,color:#fff
Step 1: Create the API Client
class ApiClient {
constructor(config) {
this.client = axios.create({
baseURL: config.baseURL,
timeout: config.timeout || 10000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
this.maxRetries = config.maxRetries || 3
this.retryDelay = config.retryDelay || 1000
this.cache = new Map()
this.pendingRequests = new Map()
this.setupInterceptors()
}
setupInterceptors() {
this.client.interceptors.request.use(
config => this.handleRequest(config),
error => Promise.reject(error)
)
this.client.interceptors.response.use(
response => this.handleResponse(response),
error => this.handleError(error)
)
}
}
Step 2: Add Request Interceptor with Auth and Deduplication
handleRequest(config) {
// Add auth token
const token = sessionStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// Add request ID
config.headers['X-Request-ID'] = crypto.randomUUID()
// Deduplicate identical GET requests
if (config.method === 'get') {
const cacheKey = this.getCacheKey(config)
const pending = this.pendingRequests.get(cacheKey)
if (pending) {
return pending
}
// Check cache
const cached = this.cache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < 60000) {
return Promise.reject({ __fromCache: true, data: cached.data })
}
}
return config
}
getCacheKey(config) {
return `${config.method}:${config.url}:${JSON.stringify(config.params || {})}`
}
Step 3: Add Response Interceptor with Caching
handleResponse(response) {
// Cache GET responses
if (response.config.method === 'get') {
const cacheKey = this.getCacheKey(response.config)
this.cache.set(cacheKey, {
data: response.data,
timestamp: Date.now()
})
}
// Return just the data for cleaner usage
return response.data
}
Step 4: Add Error Handling with Retry
async handleError(error) {
// Return cached data if available
if (error.__fromCache) {
return error.data
}
const config = error.config
// Don't retry on 4xx (client errors) except 429
if (error.response?.status >= 400 && error.response?.status !== 429) {
return Promise.reject(error)
}
// Retry logic
if (!config._retryCount) config._retryCount = 0
if (config._retryCount < this.maxRetries) {
config._retryCount++
// Exponential backoff
const delay = this.retryDelay * Math.pow(2, config._retryCount - 1)
await new Promise(r => setTimeout(r, delay))
return this.client(config)
}
return Promise.reject(error)
}
Step 5: Add Convenience Methods
async get(url, params = {}, config = {}) {
return this.client.get(url, { ...config, params })
}
async post(url, data, config = {}) {
return this.client.post(url, data, config)
}
async put(url, data, config = {}) {
return this.client.put(url, data, config)
}
async delete(url, config = {}) {
return this.client.delete(url, config)
}
async upload(url, files, onProgress) {
const formData = new FormData()
if (Array.isArray(files)) {
files.forEach(f => formData.append('files', f))
} else {
formData.append('file', files)
}
return this.client.post(url, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: onProgress
})
}
clearCache() {
this.cache.clear()
}
Step 6: Using the Client
const api = new ApiClient({
baseURL: 'https://jsonplaceholder.typicode.com',
timeout: 5000,
maxRetries: 2
})
// Basic usage
const users = await api.get('/users')
const newUser = await api.post('/users', { name: 'Alice' })
const updated = await api.put('/users/1', { name: 'Bob' })
await api.delete('/users/1')
// Upload with progress
await api.upload('/upload', file, (e) => {
console.log(`${Math.round((e.loaded * 100) / e.total)}%`)
})
Expected output: The client handles caching, deduplication, retry, and authentication automatically.
Complete Integration
The full client combines all patterns above. Use it across your application as a Singleton for consistent behavior.
Common Mistakes
1. Not clearing the request deduplication map on error
If a deduplicated request fails, all waiting promises must be rejected. Always clean up both success and failure paths.
2. Caching too aggressively
Cache invalidation is hard. Use short TTLs (30-60 seconds) and clear cache on mutation operations (POST/PUT/DELETE).
3. Infinite retry loops
Always limit retries to prevent infinite loops. Check config._retryCount and set a maximum.
4. Not distinguishing between retriable and non-retriable errors
401, 403, and 4xx validation errors should not be retried. Only retry on 5xx, 429, and network errors.
5. Mutating the original error object
Creating new error objects for cache responses avoids mutating the original error, which could affect other error handlers.
Practice Questions
Why cache GET requests? To avoid redundant network calls for unchanged data, improving perceived performance and reducing server load.
What is request deduplication? Preventing multiple identical in-flight requests by returning the same promise to all callers.
How does exponential backoff work? The delay between retries doubles each time: 1s, 2s, 4s, 8s, preventing server overload.
Which HTTP status codes should be retried? 429 (rate limit), 5xx (server errors), and network errors. 4xx (client errors) should not be retried.
How do you invalidate cache after a mutation? Clear the entire cache or delete specific keys after POST, PUT, PATCH, and DELETE operations.
Challenge
Extend the client with offline support. Queue failed requests when the browser is offline and replay them when connectivity returns. Use the Network Information API to detect online/offline status.
FAQ
What's Next
Congratulations on building a complete Axios API client! Continue with related topics:
| Tutorial | What You'll Learn |
|---|---|
| Interceptors and Error Handling | Advanced interceptor patterns and error strategies |
| Promise Patterns | Advanced promise patterns for async JavaScript |
Related topics: REST API design patterns, request deduplication and caching strategies.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro