Skip to content

Axios Project — Build a Complete Application with Axios

DodaTech Updated 2026-06-28 5 min read

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

  1. Why cache GET requests? To avoid redundant network calls for unchanged data, improving perceived performance and reducing server load.

  2. What is request deduplication? Preventing multiple identical in-flight requests by returning the same promise to all callers.

  3. How does exponential backoff work? The delay between retries doubles each time: 1s, 2s, 4s, 8s, preventing server overload.

  4. Which HTTP status codes should be retried? 429 (rate limit), 5xx (server errors), and network errors. 4xx (client errors) should not be retried.

  5. 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

How do I handle rate limiting in the client?

Detect 429 responses. Read the Retry-After header for the wait duration. Queue subsequent requests and replay after the rate limit resets.

Can this client work with GraphQL?

Yes. Modify the post method to use a consistent /graphql endpoint and structure requests with query and variables.

How do I add request logging?

Add a second request interceptor that logs method, url, timing, and response status. Include performance markers for monitoring.

Should I use singletons or instances?

Singleton is best for the main API. Create separate instances for different microservices or for integrations with third-party APIs.

How do I test this client?

Inject a mock adapter in tests. Test each interceptor separately with unit tests. Use integration tests with a real or mock server.


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