Using Axios Properly: A Practical Guide
Axios is an HTTP client for JavaScript. It runs in browsers and Node.js, and it handles requests with automatic JSON transformation, promise-based responses, and interceptors that most other libraries don't offer without extra work. It has been the default choice for React and Vue projects for years because it fills gaps that the native fetch API leaves open. You install it with npm or yarn like any other dependency: npm install axios
Or use a CDN script tag if you're not bundling: <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> The simplest request looks like this: const axios = require('axios');
axios.get('https://api.example.com/data') .then(response => console.log(response.data)); That works fine for quick scripts. In a real project you're going to want more control than that, so here's how people actually set it up.
Essential Axios Configuration Patterns
Create a default instance with your base URL, timeout, and authorization header baked in: const api = axios.create({ baseURL: 'https://api.example.com',
timeout: 10000, headers: { Authorization: 'Bearer YOUR_TOKEN' } });
Then use api.get(), api.post(), and so on. The advantage is you don't repeat the base URL or header on every call. You change the config in one place and it applies everywhere. POST requests with a JSON body are straightforward: await api.post('/users', { name: 'John', email: 'john@example.com' });
Get the Full Details

The payload is serialized automatically. You do not need to call JSON.stringify yourself. Passing an object directly is the right way to do it. If you pass a string instead, Axios treats it as raw data and sends it without a Content-Type header set to application/json, which will cause most APIs to reject the request. Handling errors properly matters more than most tutorials show. Here is the structure of an Axios error object: try {
const response = await api.get('/protected'); } catch (error) { if (error.response) {
// Server responded with a status outside 2xx range console.log(error.response.status); } else if (error.request) {
// Request was made but no response received console.log('No response from server'); } else {
// Something else went wrong console.log(error.message); }
} The three branches cover the distinct failure modes. Many developers skip this and just log the error object, which makes debugging significantly harder because the properties live in different places depending on where the failure occurred. One thing that catches people off guard: the response object contains a config property that is mutated during the request lifecycle. If you store that config object and try to reuse it later, some fields will have changed. Do not reuse the same config between separate requests without cloning it first.

Interceptors for Authentication and Logging
Request interceptors modify outgoing requests. Response interceptors modify incoming responses or handle errors globally: api.interceptors.request.use(config => { config.headers['X-Request-Id'] = generateUUID();
return config; }); api.interceptors.response.use(
response => response, error => { if (error.response?.status === 401) {
// Refresh token or redirect to login } return Promise.reject(error);
} ); Interceptors are where you centralize logic that would otherwise repeat across dozens of calls. But be careful with infinite loops. If your 401 handler triggers a token refresh and that refresh also goes through the same interceptor chain, you can easily create a loop. I had this exact problem in a project once. The token refresh endpoint was protected by the same auth header, so refreshing the token meant calling the refresh endpoint, which failed with 401, which triggered another refresh attempt, and the cycle repeated until the request timed out. The fix was to add a flag to the request config so the interceptor would skip its logic on the refresh call itself:
config.headers['X-Auth-Refresh'] = true; And in the interceptor, check for that flag before running the auth logic. Response interceptors also run in order. The first one you attach processes the response first, then passes it to the next. This means if you have one interceptor that transforms the response data and another that logs it, the log interceptor sees the already-transformed data. That is usually fine but it can be surprising if you are not expecting it.

Axios Cancellation and Concurrency
Cancellation is done through an AbortController. Axios accepts an AbortSignal directly: const controller = new AbortController(); axios.get('/data', { signal: controller.signal });
// Then later: controller.abort(); This works in both browsers and modern Node.js. For older environments you would need the axios.CancelToken API, but that is deprecated and you should not use it in new code.
For running multiple requests in parallel, Promise.all works fine: const [users, posts] = await Promise.all([ api.get('/users'),
api.get('/posts') ]); If you want to cancel all requests when one fails, use Promise.allSettled instead and manage the controllers manually. There is no built-in "cancel all" feature in Axios, and trying to chain cancellation through interceptors tends to break in unexpected ways.
One detail that matters for performance: Axios does not cache responses. Every call to api.get('/users') makes a fresh network request. This is different from service workers or some GraphQL clients that cache automatically. If you need caching behavior, you have to implement it yourself or use a library like React Query or SWR on top of Axios.
Common Mistakes and Their Workarounds
Mistake one: sending form data as JSON when the API expects application/x-www-form-urlencoded. Axios sends JSON by default for object payloads. If the backend expects form-encoded data, either set the correct Content-Type header and encode the body yourself, or use the qs library to serialize the data properly before passing it. Mistake two: assuming error.response.data always exists. On a network failure or timeout, error.response is undefined and error.request is set instead. Accessing error.response.data without checking will throw a TypeError. Always check which property is present first. Mistake three: using Axios in environments where bundle size matters and fetch is available. Axios adds roughly 13kb minified to your bundle. If you are building a small utility or a mobile app where every kilobyte counts, the native fetch API with a lightweight wrapper may be a better choice. Libraries like ky (about 3kb) or the built-in fetch with a small helper function can replace most Axios use cases without the overhead.

Another issue specific to browser usage: CORS. Axios does not solve CORS problems for you. If your API returns missing or incorrect Access-Control headers, Axios will throw the same error that fetch would throw. The solution is to fix the server configuration, not the client. Common fixes include adding Access-Control-Allow-Origin to the response and making sure preflight OPTIONS requests are handled by the server. I ran into a situation where a staging API was returning gzip-compressed responses but Axios was failing to decompress them. The server had misconfigured its Accept-Encoding handling. The workaround was to disable compression for that specific endpoint by setting headers: { 'Accept-Encoding': 'identity' } on the request. It is not ideal, but it unblocked the integration while the server issue was being fixed.
Advanced Usage: File Uploads and Progress Tracking
Uploading files requires passing a FormData object as the request body: const formData = new FormData(); formData.append('file', fileInput.files[0]);
await axios.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: progressEvent => {
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total); console.log(percent + '%'); }
}); You must set the Content-Type header manually when using FormData, or Axios will set it to application/json by default and the upload will fail. The onUploadProgress callback gives you real-time feedback on upload progress, which is useful for showing a progress bar in a UI. For downloading large files, Axios can stream the response if you set responseType: 'stream' in Node.js, or responseType: 'blob' in the browser. Using the wrong responseType will cause the data to be parsed as text or JSON and will corrupt binary files.
One nuance about Axios and cookies: in browsers, Axios sends cookies automatically because it uses XHR under the hood. In Node.js, cookies are not sent unless you explicitly configure a cookie jar using a library like tough-cookie. If you are switching between browser and Node.js environments, this difference will cause authentication to work in one but not the other. The library has been stable and well-maintained for over a decade. It is not going away, and it handles the edge cases that raw fetch does not. But it is not a silver bullet. For simple GET requests in a modern browser, fetch is sufficient. For complex applications with token refresh flows, global error handling, and request interception, Axios saves significant development time. The trade-off is bundle size and a slightly steeper learning curve for the error handling pattern. Once you internalize the error object structure and how interceptors chain together, the workflow becomes second nature.
.svg/1200px-Axios_logo_(2017).svg.png)