Hello everyone! As a Frontend Developer recently working on a delivery app, my team lead asked me to compare Axios and Ky to decide which would be better for our project. I ended up writing this article to summarize my findings. If you are also unsure about which library to pick for your next project, I believe this comparison will definitely help you! And not to forget this is my first article on Dev.io, I am excited to continue sharing my experiences and the things I learn along my journey.
Ky VS Axios
When we are using React for developing apps, we definitely need a good library to handle HTTP requests. The simplest way to handle these requests is the Fetch API. Fetch API is perfectly usable for complex apps, but libraries like Axios or Ky provide convenience features such as automatic JSON parsing, retries, and request abstraction. In this article, we will look at some features of Ky and Axios to see which one is a better choice for our scenarios.
First, let’s look at Axios, a very popular library with over 300 million downloads per month (according to the npm website). Axios is a promise-based HTTP Client that is isomorphic, meaning we can run it in the browser and Node.js with the same codebase. On the server-side it uses the native Node.js HTTP module, while on the client (browser) it uses XMLHttpRequest.
Secondly, Ky is a small, modern library built based on modern Fetch API. It targets modern browsers and Ky works in Node.js environments that support the Fetch API (Node 18+ or with a polyfill).
First let’s look at a simple example on how a simple request is made with axios and ky.
Axios
import axios from 'axios';
const fetchData = async () => {
try {
const response = await axios.get('https://api.example.com/users/1');
console.log(response.data); // Data is already parsed here
} catch (error) {
console.log(error);
}
};
In Axios, the response is automatically parsed. The data we want is always located inside the .data property of the response object.
Ky
import ky from 'ky';
const fetchData = async () => {
try {
const user = await ky.get('https://api.example.com/users/1').json();
console.log(user); // 'user' is the parsed JSON object
} catch (error) {
console.error(error);
}
};
In Ky, we use a chainable method like .json() to parse the response. This is because Ky is built on Fetch and handles the response stream explicitly and this is one of the biggest differences of axios and ky.
Let’s compare these two from different aspects, the considered aspects are meant to be the important characteristics of a HTTP library.
1. Size
In case of bundle size in production, Axios adds about 14 KB to the bundle size while Ky adds only about 5 KB. So, if we want to keep our bundle light, Ky wins over Axios.
2. Error Handling
Both Axios and Ky are great at handling errors because they recognize non-2XX error codes. This helps us manage errors received from the server very well.
• Axios: It rejects every response that falls out of the range of status >= 200 && < 300 by default. However, with the validateStatus option, we can customize exactly which status codes Axios should reject.
Example:
axios.get("/user/12345", {
validateStatus: function (status) {
return status < 500; // Only reject if the status is 500 or higher
},
});
When an error occurs, Axios gives us helpful properties like message, code, and response. The code property is great for debugging because it tells us exactly what went wrong, like ERR_NETWORK or ERR_BAD_RESPONSE.
• Ky: Ky also rejects non-2XX responses by throwing an HTTPError. I think the most outstanding feature for in Ky is the automatic retry feature. It retries our request in case we get network error or some other status codes two times by default. To see the details of a server error, we can simply use await error.response.json().
3. Interceptors (The Guards)
First, let’s define what an interceptor is. You might be familiar with this term, but let’s briefly recall it. An interceptor is like a guard: it checks our request before we send it to the server, and after we get a response, it checks that response as well.
The most useful cases for interceptors are:
• Automatically attaching headers (like Content-Type).
• Adding Authorization tokens to every request.
• Handling specific errors globally, such as refreshing an expired token.
Request and Response Interceptors in Axios
Request interceptor
In Axios we can use request interceptor to modify the request data before sent to the server like attaching header, and authorization. It is done by using axios.interceptors.request.use. Below is a real-life example of a request interceptor in axios. That is attaching the header, and authorization.
import axios from "axios";
import useAuthStore from "../src/store/useAuthStore";
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
withCredentials: true
});
api.interceptors.request.use(
(config) => {
const token = useAuthStore.getState().token
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
if (!(config.data instanceof FormData)) {
config.headers["Content-Type"] = "application/json";
}
return config;
},
(error) => Promise.reject(error)
);
Response Interceptor
Axios uses axios.interceptors.response.use to allow us to change the response data when received for example checking if we get a 401 error and request a token immediately. Below is a real-life example of response interceptor:
api.interceptors.response.use(
(response) => {
return response
},
async (error) => {
const originalRequest = error.config
if (error.response?.status === 401 && !originalRequest._retry ) {
originalRequest._retry = true
try {
const response = await axios.post(
`${import.meta.env.VITE_API_BASE_URL}/auth/refresh`,
{},
{ withCredentials: true }
);
const accessToken = response.data.token
useAuthStore.getState().login(accessToken, null)
originalRequest.headers.Authorization = `Bearer ${accessToken}`
return api(originalRequest)
} catch (refreshError) {
useAuthStore.getState().logout();
return Promise.reject(refreshError)
}
}
return Promise.reject(error)
}
)
Ky beforeRequest and afterResponse hooks
Ky uses a hooks system instead of interceptors. These hooks act as guards for requests and responses. The key difference is that Ky hooks can be defined per instance or per request, making it easy to customize behavior for specific requests.
In Axios, interceptors are global or instance-based. While it is possible to customize behavior per request, it usually requires adding custom flags and conditional logic inside interceptors, which can become harder to maintain. Ky provides a cleaner and more composable approach in such scenarios.
beforeRequest hook
This hook acts like a request interceptor. We use it to attach headers or authorization. If we handle the same scenario you wrote for Axios, it would look like this in Ky:
hooks: {
beforeRequest: [
(request) => {
const token = useAuthStore.getState().token;
if (token) {
request.headers.set('Authorization', `Bearer ${token}`);
}
}
]
}
Note: the error handling part that we have in axios is handled differently in ky. If something goes wrong with the request it rejects the promise automatically.
afterResponse hook
This is where Ky handles the response guard. A key difference here is that Ky's hooks are arrays, so we can have multiple functions running one after another.
hooks: {
afterResponse: [
async (request, options, response) => {
if (response.status === 401 && !options.retryCount) {
try {
const refreshResponse = await ky.post(
`${import.meta.env.VITE_API_BASE_URL}/auth/refresh`,
{ withCredentials: true }
).json();
const accessToken = refreshResponse.token;
useAuthStore.getState().login(accessToken, null);
request.headers.set('Authorization', `Bearer ${accessToken}`);
return ky(request);
} catch (refreshError) {
useAuthStore.getState().logout();
throw refreshError;
}
}
return response;
}
]
}
4. Compatibility
This is a big factor depending on who will use our app. Axios is based on the older XMLHttpRequest, so it works on almost every browser, even old ones like Internet Explorer 11.
Ky is built on the modern Fetch API, so it only targets modern browsers. If the project needs to support very old systems, Ky might not be the right choice without extra work.
5. Some other differences
TypeScript support
Axios: good
Ky: excellent (very clean typings)
Cancellation
Axios: CancelToken (legacy) + AbortController (modern)
Ky: uses AbortController (native)
Ecosystem
Axios: huge ecosystem
Ky: minimalistic
6. Summary
| Feature | Axios | Ky |
|---|---|---|
| Foundation | XMLHttpRequest / Fetch | Fetch API (Modern) |
| Bundle Size | ~14 KB (Gzip) | ~5 KB (Gzip) |
| Automatic Retries | No (Requires Plugins) | Yes (Built-in) |
| Logic Style | Global Interceptors | Composable Hooks |
| Legacy Support | High (Works in IE11) | Modern Browsers only |
7. When to use which?
Use Axios when you need maximum compatibility, a mature ecosystem, or are working with older environments.
Use Ky when building modern applications that rely on the Fetch API and benefit from a smaller bundle size and cleaner API design.
Resources I used for writing this article:
Ky:
https://www.npmjs.com/package/@smeijer/ky?activeTab=readme
https://github.com/sindresorhus/ky
Axios:
https://www.npmjs.com/package/axios?activeTab=readme
https://axios-http.com/docs/intro
What about you? Which library are you using currently and which one do you prefer?
Top comments (0)