axios
Promise-based HTTP client that runs in both the browser (via XMLHttpRequest) and Node.js (via the http module). One API for all request methods with automatic JSON serialization, request and response interceptors, upload and download progress events, cancellation via AbortController, and built-in encoding for multipart/form-data and urlencoded bodies. It sat in nearly every JavaScript project before fetch went universal, and its interceptor model is still the main reason teams keep reaching for it.
Still the safe boring choice when you need interceptors or wide runtime coverage. For new projects on Node 18+ making plain JSON calls, native fetch or ky does the same job without the dependency.
Use it if
- You want request/response interceptors for auth tokens, logging, or error normalization without wrapping fetch yourself
- You need one HTTP client that behaves identically in browsers and Node, including Node versions without a stable native fetch
- You want automatic JSON parsing, query param serialization, and multipart/form-data encoding out of the box
- You need upload or download progress events, which plain fetch still makes awkward
- You are on Node 18+ or modern browsers and only make simple JSON requests: native fetch does the job with zero dependencies
- You call servers you do not fully trust and will not tune config: maxContentLength and maxBodyLength default to unlimited, so a decompression bomb can exhaust your Node process unless you set caps yourself (the README now warns about this)
- You need HTTP/2: axios support is experimental in the Node adapter and behavior varies by runtime
- You need retries: there is no built-in retry; you have to add axios-retry or write your own interceptor
- Bundle size matters: at roughly 18 KB gzipped it is many times heavier than ky or bare fetch
Setup reality
npm install axios and it works: no peer dependencies, no config, TypeScript types included. The real work starts when you productionize. Nothing retries by default, timeout defaults to 0 (wait forever), and response size limits default to unlimited, so a serious setup means an axios.create() instance with baseURL, timeout, maxContentLength, and interceptors. Two current gotchas: CancelToken is deprecated in favor of AbortController, and some strict backends reject axios's readable query encoding until you override paramsSerializer.encode with encodeURIComponent.
Patterns
Fetch JSON with error-aware handlingget-json
import axios from 'axios';
const { data, status } = await axios.get('https://api.example.com/users/1');
console.log(status, data);axios parses JSON automatically and rejects on non-2xx status, unlike fetch which resolves on 404s.
POST a JSON bodypost-json
const res = await axios.post('/api/users', {
name: 'Ada',
email: 'ada@example.com',
});
console.log(res.data.id);Plain objects are serialized to JSON with the right Content-Type header; no JSON.stringify needed.
Create a configured instancecreate-instance
const api = axios.create({
baseURL: 'https://api.example.com/v1',
timeout: 10000,
maxContentLength: 10 * 1024 * 1024,
headers: { 'X-Client': 'my-app' },
});
const { data } = await api.get('/orders');timeout defaults to 0 (never) and maxContentLength to unlimited; set both on every production instance.
Attach an auth token with a request interceptorauth-interceptor
api.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${getToken()}`;
return config;
});Request interceptors run last-registered-first; response interceptors run in registration order.
Distinguish HTTP errors from network errorshandle-errors
try {
await api.get('/thing');
} catch (err) {
if (axios.isAxiosError(err)) {
if (err.response) {
console.error('HTTP', err.response.status, err.response.data);
} else if (err.request) {
console.error('No response (network/timeout)', err.code);
}
} else {
throw err;
}
}err.response exists only when the server answered; timeouts surface as code ECONNABORTED (or ETIMEDOUT with clarifyTimeoutError).
Cancel a request with AbortControllertimeout-cancel
const controller = new AbortController();
const promise = axios.get('/slow', {
signal: controller.signal,
timeout: 5000,
});
// somewhere else
controller.abort();Use AbortController; the older CancelToken API is deprecated since v0.22 and should not appear in new code.
Retry a failed request with an interceptorhttp-retry
api.interceptors.response.use(undefined, async (error) => {
const cfg = error.config;
cfg.__retries = (cfg.__retries || 0) + 1;
const retryable = !error.response || error.response.status >= 500;
if (retryable && cfg.__retries <= 3) {
await new Promise((r) => setTimeout(r, 300 * 2 ** cfg.__retries));
return api.request(cfg);
}
return Promise.reject(error);
});axios has no built-in retry; for anything beyond this, use the axios-retry package instead of growing this interceptor.
Send query parametersquery-params
await axios.get('/search', {
params: { q: 'printers', page: 2 },
// strict RFC 3986 encoding for picky backends:
paramsSerializer: { encode: encodeURIComponent },
});By default axios keeps characters like : and $ readable in query strings; some backends require the strict encoding shown here.
Upload a file as multipart/form-dataupload-formdata
const form = new FormData();
form.append('file', fileInput.files[0]);
form.append('caption', 'invoice scan');
await axios.post('/upload', form, {
onUploadProgress: (e) => {
if (e.total) console.log(Math.round((e.loaded / e.total) * 100) + '%');
},
});Do not set the Content-Type header yourself; axios sets the multipart boundary automatically.
Track download progressdownload-progress
const res = await axios.get('/big-file.zip', {
responseType: 'blob',
onDownloadProgress: (e) => {
if (e.total) console.log(e.loaded, 'of', e.total);
},
});e.total is undefined when the server omits Content-Length; guard before computing percentages.
Cap response size against decompression bombslimit-response-size
axios.defaults.maxContentLength = 10 * 1024 * 1024; // 10 MB
axios.defaults.maxBodyLength = 10 * 1024 * 1024;Both default to -1 (unlimited); a hostile server can return a tiny gzip body that expands to gigabytes in your Node process.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| ky | npm | Browser-first fetch wrapper at a fraction of the size, with retries built in |
| got | npm | Node-only projects that want retries, hooks, and streams without plugins |
| node-fetch | npm | You want the plain fetch API shape on Node versions lacking stable native fetch |