I was curios about how axios internally works, so I tried building a mini version of it.
Just enough to understand how it works under the hood.
While building, I learnt: interceptors, config merging, timeouts, aborting requests.
Let’s walk through it.
What We’re Trying to Recreate
Axios gives you a nice set of features:
- Global configuration
- Request & response interceptors
- Timeout support
- Aborting a request
- Clean, chainable promises
That’s the entire goal — a toy model that captures these ideas, without the complexity.
Step 1: The MiniAxios Blueprint
Think of it as a little factory.
class MiniAxios {
config = {
timeout: 1000,
headers: { "Content-Type": "application/json" },
};
requestInterceptors = [];
responseInterceptors = [];
constructor(config) {
this.config = this.mergeConfig(config);
}
}
Step 2: The Request Pipeline
Axios’s “magic” is just a long promise chain where every interceptor gets a chance to modify something.
async request({ url, config }) {
const chain = [
...this.requestInterceptors,
{ successFn: this.dispatchRequest.bind(this, url) },
...this.responseInterceptors,
];
let promise = Promise.resolve({ ...config });
chain.forEach(({ successFn, errorFn }) => {
promise = promise.then(
(res) => successFn(res),
(err) => errorFn && errorFn(err)
);
});
return promise;
}
Step 3: The Network Layer (with Timeout + Abort)
When axios says “timeout”, it’s really just an AbortController waiting to cancel fetch.
async dispatchRequest(url, config) {
const abortController = new AbortController();
const finalConfig = this.mergeConfig(config);
const timeout = finalConfig.timeout || 0;
let timeoutId;
if (timeout) {
timeoutId = setTimeout(() => abortController.abort(), timeout);
}
try {
return await fetch(`${finalConfig.baseUrl}${url}`, {
...finalConfig,
signal: abortController.signal,
});
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
A tiny timeout implementation. No heavy lifting.
Just: setTimeout → abort → fetch throws → promise rejects.
Step 4: Adding Interceptors
Axios lets you plug custom logic into the pipeline. we do the same — just store functions.
addRequestInterceptor(successFn, errorFn) {
this.requestInterceptors.push({ successFn, errorFn });
}
addResponseInterceptor(successFn, errorFn) {
this.responseInterceptors.push({ successFn, errorFn });
}
This lets you:
- Add auth tokens
- Log requests
- Retry failed responses
Format data
Everything fits into the same pipeline.
Step 5: A Simple get
async get(url, config = {}) {
return this.request({
url,
config: { ...config, method: "GET" },
});
}
Axios has more helpers, but the shape is identical.
Step 6: Creating an API Instance
const MyApi = MiniAxios.create({
baseUrl: "https://jsonplaceholder.typicode.com",
timeout: 1000,
});
Just like axios.create().
You configure once, use everywhere.
Step 7: Adding a Request Interceptor
MyApi.addRequestInterceptor((config) => {
console.log("request interceptor", config);
return {
...config,
"x-api-key": "testing-req-interceptor",
};
});
One place to attach your headers. Super useful for auth.
Using It in React
useEffect(() => {
(async () => {
const res = await MyApi.get("/todos");
const data = await res.json();
setTodos(data.slice(-5));
})();
}, []);
Still the same fetch response under the hood, so .json() works exactly as expected.
The entire program flow:
- Start with default config
- Pipe it through request interceptors
- Eventually hit dispatchRequest
- Pipe the result through response interceptors
Here’s a complete codesandbox link