All topics
Frontend · Learning hub

Axios notes for developers

Master Axios with a curated set of 1 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallTest yourself — Axios quizMore Frontend notes
Axios

Axios Essentials

Axios Essentials Axios is a promise-based HTTP client for the browser and Node.js. Compared to the native `fetch`, it auto-parses JSON, throws on non-2xx status

Axios Essentials

Axios is a promise-based HTTP client for the browser and Node.js. Compared to the native `fetch`, it auto-parses JSON, throws on non-2xx status codes by default, supports request/response interceptors, has built-in request cancellation, and normalizes behavior across browsers and Node — which is why it's still the default choice in most React/Vue apps even though `fetch` has closed much of the gap.

Instances & Base Config

Never call the default `axios` export directly across a real app — create a configured instance per API you talk to. This centralizes the base URL, default headers, and timeout, and gives you a single place to attach interceptors.

// api/client.js
import axios from 'axios'

export const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL ?? 'https://api.example.com',
  timeout: 10000, // ms — fail fast instead of hanging forever
  headers: {
    'Content-Type': 'application/json',
  },
  // withCredentials: true, // send cookies cross-origin (needs matching CORS config server-side)
})

// Per-request overrides merge with instance defaults
api.get('/users', { params: { page: 2, limit: 20 } })
api.get('/users/42', { timeout: 5000 })

// Query params: axios serializes the params object into a query string
// GET /users?page=2&limit=20

// Multiple instances for multiple backends
export const authApi = axios.create({ baseURL: 'https://auth.example.com' })
export const analyticsApi = axios.create({
  baseURL: 'https://analytics.example.com',
  headers: { 'X-Client': 'web' },
})

Interceptors: Auth Headers & Token Refresh

Request interceptors run before a request is sent (attach auth headers, log, mutate config). Response interceptors run on every response, including errors — the classic use case is transparently refreshing an expired access token and retrying the original request exactly once, so callers never see the 401.

import { api } from './client'
import { getAccessToken, refreshAccessToken, logout } from './auth'

// Request interceptor — attach the current token to every call
api.interceptors.request.use(
  (config) => {
    const token = getAccessToken()
    if (token) config.headers.Authorization = `Bearer ${token}`
    return config
  },
  (error) => Promise.reject(error),
)

// Response interceptor — refresh once on 401, then retry the original request
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const original = error.config

    if (error.response?.status === 401 && !original._retry) {
      original._retry = true // guard against infinite refresh loops
      try {
        const newToken = await refreshAccessToken()
        original.headers.Authorization = `Bearer ${newToken}`
        return api(original) // re-run the original request with the new token
      } catch (refreshError) {
        logout()
        return Promise.reject(refreshError)
      }
    }

    return Promise.reject(error)
  },
)

// Interceptors can be removed if you need to (e.g. in tests)
const id = api.interceptors.request.use((c) => c)
api.interceptors.request.eject(id)

Error Handling

Axios rejects the promise for any non-2xx response — you don't need to manually check `response.ok` like with `fetch`. The rejected error carries three distinct shapes depending on what failed: a server responded with an error status, the request was sent but no response came back, or the request never got sent.

import axios from 'axios'
import { api } from './client'

async function createUser(payload) {
  try {
    const { data } = await api.post('/users', payload)
    return data
  } catch (error) {
    if (axios.isAxiosError(error)) {
      if (error.response) {
        // Server responded with a status outside 2xx
        console.error('Server error:', error.response.status, error.response.data)
        if (error.response.status === 422) {
          throw new ValidationError(error.response.data.errors)
        }
      } else if (error.request) {
        // Request was made, no response received (network down, timeout, CORS)
        console.error('No response received:', error.message)
      } else {
        // Something went wrong setting up the request itself
        console.error('Request setup error:', error.message)
      }
    }
    throw error
  }
}

// Treat specific non-2xx codes as success if your API uses them intentionally
api.get('/status', {
  validateStatus: (status) => status < 500, // don't reject on 4xx
})

Cancellation with AbortController

Modern axios (v1+) uses the standard `AbortController` for cancellation instead of the deprecated `CancelToken` API. This matters most for search-as-you-type and any effect that can re-fire before the previous request finishes — without cancellation, a slow earlier response can overwrite a newer one (a race condition).

import { useEffect, useState } from 'react'
import { api } from './client'

function useSearch(query) {
  const [results, setResults] = useState([])

  useEffect(() => {
    if (!query) return

    const controller = new AbortController()

    api
      .get('/search', { params: { q: query }, signal: controller.signal })
      .then((res) => setResults(res.data))
      .catch((err) => {
        if (axios.isCancel(err) || err.name === 'CanceledError') return // expected, ignore
        console.error(err)
      })

    return () => controller.abort() // cancel the in-flight request on re-run/unmount
  }, [query])

  return results
}

// Manual timeout-driven cancellation
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000)
api.get('/slow-endpoint', { signal: controller.signal }).finally(() => clearTimeout(timeoutId))

Transforming Requests & Responses

`transformRequest`/`transformResponse` run before axios serializes a request body or after it parses a response, letting you normalize payloads (snake_case ↔ camelCase, date parsing) in one place instead of at every call site.

import axios from 'axios'
import camelcaseKeys from 'camelcase-keys'
import snakecaseKeys from 'snakecase-keys'

export const api = axios.create({
  baseURL: 'https://api.example.com',
  transformRequest: [
    (data) => (data ? snakecaseKeys(data, { deep: true }) : data),
    ...axios.defaults.transformRequest, // keep default JSON.stringify step
  ],
  transformResponse: [
    ...axios.defaults.transformResponse,
    (data) => (data && typeof data === 'object' ? camelcaseKeys(data, { deep: true }) : data),
  ],
})

// Concurrent requests
const [users, posts] = await Promise.all([api.get('/users'), api.get('/posts')])

// File upload with progress
api.post('/upload', formData, {
  headers: { 'Content-Type': 'multipart/form-data' },
  onUploadProgress: (event) => {
    const percent = Math.round((event.loaded * 100) / (event.total ?? 1))
    console.log(`Upload: ${percent}%`)
  },
})

Common Gotchas

  • Forgetting axios throws on error status — unlike `fetch`, a 404/500 response rejects the promise; wrap calls in try/catch instead of checking `response.ok`.

  • Using the deprecated CancelToken API — v1+ uses the standard `AbortController`/`signal`; `CancelToken.source()` still works but is legacy.

  • Retrying inside a response interceptor without a guard flag — a 401 handler that always retries without checking `config._retry` can loop forever if the refresh itself keeps failing with 401.

  • Setting `Content-Type: multipart/form-data` manually with FormData — the browser needs to set the boundary itself; let axios/the browser generate this header for `FormData` bodies rather than hardcoding it.

  • No default timeout — axios requests hang indefinitely unless you set `timeout` on the instance; a slow backend can otherwise stall your UI with no feedback.

  • Calling the default `axios` export everywhere — skips the benefit of instances entirely; you end up repeating baseURL/headers/interceptor logic at every call site.

Keep your Axios knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever