Axios: HTTP Client
Axios is a promise-based HTTP client that runs in both Node.js and the browser, wrapping the lower-level request machinery in a small, ergonomic API. It became popular for features the platform historically lacked — automatic JSON parsing, request and response interceptors, configurable instances with a shared baseURL, and timeouts — and it remains a productive choice for talking to REST APIs. This page covers GET/POST requests, interceptors, default configuration, timeouts, error handling, and how Axios stacks up against the now-built-in fetch.
Installing and a first request
Axios runs on any maintained Node.js release; Node 20 or 22 LTS is the sensible default. Install it from npm and import it as an ES module (it also ships CommonJS, so const axios = require("axios") works unchanged).
npm install axios
A GET request returns a promise that resolves to a response object. The parsed body lives on response.data — Axios deserializes JSON for you, so there is no separate parse step.
import axios from "axios";
const response = await axios.get("https://api.github.com/repos/nodejs/node");
console.log(response.status); // 200
console.log(response.data.full_name); // "nodejs/node"
console.log(response.headers["content-type"]);
Output:
200
nodejs/node
application/json; charset=utf-8
The response carries data, status, statusText, headers, and the config that produced it — useful for logging and debugging.
GET and POST
Query parameters go in a params object rather than being hand-concatenated, and Axios encodes them safely. For writes, pass a body as the second argument; objects are serialized to JSON and the Content-Type header is set automatically.
// GET /search?q=node&per_page=5
const { data } = await axios.get("https://api.github.com/search/repositories", {
params: { q: "node", per_page: 5 },
});
console.log(data.total_count);
// POST a JSON body
const created = await axios.post(
"https://httpbin.org/post",
{ title: "Learn Axios", done: false },
{ headers: { "X-Request-Source": "devcraftly" } },
);
console.log(created.data.json);
Output:
12873456
{ title: 'Learn Axios', done: false }
The companion methods axios.put, axios.patch, and axios.delete follow the same shape.
Default config and instances
Rather than repeating a host and headers on every call, create a configured instance with axios.create. Every request from that instance inherits the baseURL, default headers, and timeout, so call sites stay short and consistent.
const api = axios.create({
baseURL: "https://api.example.com/v1",
timeout: 5000,
headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
});
const users = await api.get("/users"); // -> GET https://api.example.com/v1/users
await api.post("/users", { name: "Ada" });
Prefer per-instance config over mutating the global
axios.defaults. A dedicated instance keeps each integration’s settings isolated and avoids surprising other parts of the app.
Interceptors
Interceptors let you run logic on every request or response — attaching auth tokens, logging timings, or normalizing errors in one place. A request interceptor receives the config before it is sent; a response interceptor receives the response (or, in its second callback, the error) before it reaches your await.
api.interceptors.request.use((config) => {
config.metadata = { start: Date.now() };
return config;
});
api.interceptors.response.use(
(response) => {
const ms = Date.now() - response.config.metadata.start;
console.log(`${response.config.url} -> ${response.status} (${ms}ms)`);
return response;
},
(error) => {
// Centralize error shaping; rethrow so callers still see the failure.
console.error("Request failed:", error.message);
return Promise.reject(error);
},
);
Output:
/users -> 200 (143ms)
Timeouts and error handling
The timeout option (in milliseconds) aborts a request that stalls, raising an error with code === "ECONNABORTED". Axios treats any non-2xx status as a rejection by default, so you handle HTTP and network failures with the same try/catch. Inspect error.response to distinguish a server error (the server replied) from a network or timeout error (no response).
try {
const { data } = await api.get("/users/999", { timeout: 2000 });
console.log(data);
} catch (error) {
if (error.response) {
// Server responded with 4xx/5xx
console.error("HTTP", error.response.status, error.response.data);
} else if (error.code === "ECONNABORTED") {
console.error("Request timed out");
} else {
console.error("Network error:", error.message);
}
}
Output:
HTTP 404 { message: 'Not Found' }
Axios vs. native fetch
Node 18+ ships a global fetch, so a dependency is no longer mandatory for HTTP. The trade-off is convenience versus footprint.
| Feature | Axios | Native fetch |
|---|---|---|
| JSON parsing | Automatic via response.data | Manual await res.json() |
| Non-2xx handling | Rejects the promise | Resolves; check res.ok yourself |
| Interceptors | Built in | Roll your own wrapper |
baseURL / instances | axios.create | Not built in |
| Timeouts | timeout option | AbortSignal.timeout(ms) |
| Dependency | External package | Built into Node 18+ |
For a couple of simple calls, fetch keeps the dependency tree lean. For apps making many requests that need shared config, interceptors, and uniform error handling, Axios removes a lot of boilerplate.
Best practices
- Create one configured instance per upstream API with
axios.create; avoid mutating global defaults. - Pass query strings through
paramsinstead of building URLs by hand — Axios handles encoding. - Always set a
timeoutso a hung dependency cannot stall your service indefinitely. - Centralize auth and logging in interceptors, but rethrow errors so callers can still react.
- In
catchblocks, branch onerror.responsevs.error.codeto tell HTTP errors from network failures. - Keep secrets like API tokens in environment variables, not in source.
- If you only make a few requests with no shared config, weigh native
fetchto drop the dependency.