API Client Libraries: Axios vs ky vs ofetch in 2026
Every JavaScript application makes HTTP requests. The question is: should you use the built-in fetch API, or reach for a library? And if a library, which one?
We compared Axios, ky, and ofetch — the three most popular HTTP client libraries — using data from PkgPulse.
Why Not Just Use fetch?
Native fetch is available everywhere in 2026 — browsers, Node.js 22, Deno, and Bun. But it has ergonomic gaps:
// Native fetch — verbose, no automatic error handling
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice' }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json(); // Type: any
Libraries fix these pain points: automatic JSON handling, error throwing on non-2xx responses, retries, interceptors, and TypeScript generics.
The Contenders
| Library | Weekly Downloads | Size (gzip) | Built On |
|---|---|---|---|
| Axios | 50M+ | 13KB | XMLHttpRequest + Node adapter |
| ky | 2M | 3.3KB | Native fetch |
| ofetch | 8M | 3KB | Native fetch |
Axios
The veteran. Axios has been the default HTTP client since 2014. Massive ecosystem, familiar API, works everywhere.
import axios from 'axios';
// POST with automatic JSON serialization
const { data } = await axios.post<User>('/api/users', {
name: 'Alice',
email: 'alice@example.com',
});
// Interceptors for auth
axios.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${token}`;
return config;
});
Strengths
- Massive ecosystem — Most tutorials and examples use Axios
- Interceptors — Request and response interceptors for auth, logging, etc.
- Request cancellation — Via AbortController (and legacy CancelToken)
- Progress tracking — Upload and download progress events
- Browser + Node — Works in both environments with environment-specific adapters
Weaknesses
- Large bundle — 13KB gzipped, 4x larger than alternatives
- Based on XMLHttpRequest (browser) — Not built on modern fetch API
- Feature bloat — Many features most projects don't use
- Slower development — Fewer updates compared to newer alternatives
ky
The modern, tiny HTTP client from Sindre Sorhus. Built on native fetch, ky adds just the features you need — retries, JSON shortcuts, hooks — in 3.3KB.
import ky from 'ky';
// POST with automatic JSON
const data = await ky.post('/api/users', {
json: { name: 'Alice', email: 'alice@example.com' },
}).json<User>();
// Retries with exponential backoff
const data = await ky.get('/api/data', {
retry: { limit: 3, methods: ['get'] },
}).json();
// Hooks (like interceptors)
const api = ky.create({
prefixUrl: 'https://api.example.com',
hooks: {
beforeRequest: [(request) => {
request.headers.set('Authorization', `Bearer ${token}`);
}],
afterResponse: [(_request, _options, response) => {
if (response.status === 401) refreshToken();
}],
},
});
Strengths
- Tiny — 3.3KB gzipped (4x smaller than Axios)
- Built on fetch — Modern, standard API underneath
- Smart retries — Exponential backoff with configurable methods
- Hooks — Before/after request hooks (cleaner than interceptors)
- TypeScript — Excellent type support
Weaknesses
- Browser only — No Node.js support (use
ky-universalor ofetch for universal) - Smaller ecosystem — Fewer guides and community resources
- No progress tracking — fetch doesn't support upload progress natively
ofetch
Universal fetch from the UnJS ecosystem (the team behind Nuxt). Works identically in browsers, Node.js, Deno, Bun, and edge runtimes.
import { ofetch } from 'ofetch';
// POST with automatic JSON
const data = await ofetch<User>('/api/users', {
method: 'POST',
body: { name: 'Alice', email: 'alice@example.com' },
});
// Retries built-in
const data = await ofetch('/api/data', {
retry: 3,
retryDelay: 500,
});
// Interceptors
const api = ofetch.create({
baseURL: 'https://api.example.com',
onRequest({ options }) {
options.headers.set('Authorization', `Bearer ${token}`);
},
onResponseError({ response }) {
if (response.status === 401) refreshToken();
},
});
Strengths
- Universal — Same code runs in browsers, Node.js, Deno, Bun, Workers
- Tiny — 3KB gzipped
- Built on fetch — Modern standard
- Auto-detection — Automatically sets Content-Type, parses JSON responses
- Smart retries — Configurable retry logic
- UnJS ecosystem — Used by Nuxt, Nitro, and other popular frameworks
Weaknesses
- Smaller community — Fewer guides outside the Nuxt ecosystem
- Less feature-rich — No upload progress, fewer advanced options
- Interceptor API — Less flexible than Axios's interceptor chain
Feature Comparison
| Feature | Axios | ky | ofetch |
|---|---|---|---|
| Size (gzip) | 13KB | 3.3KB | 3KB |
| Built on | XHR + adapters | fetch | fetch |
| Browser | ✅ | ✅ | ✅ |
| Node.js | ✅ | ❌ | ✅ |
| Edge runtimes | ❌ | ✅ | ✅ |
| Auto JSON | ✅ | ✅ | ✅ |
| Retries | Plugin | ✅ Built-in | ✅ Built-in |
| Interceptors | ✅ (rich) | Hooks | ✅ |
| Upload progress | ✅ | ❌ | ❌ |
| Timeout | ✅ | ✅ | ✅ |
| TypeScript | ✅ | ✅ | ✅ |
| AbortController | ✅ | ✅ | ✅ |
| Streaming | ❌ | ✅ | ✅ |
Performance Comparison
For 1,000 sequential API calls:
| Library | Total Time | Bundle Cost |
|---|---|---|
| Native fetch | 8.2s | 0KB |
| Axios | 8.5s | 13KB |
| ky | 8.3s | 3.3KB |
| ofetch | 8.2s | 3KB |
Runtime performance differences are negligible — the network is the bottleneck. The real difference is bundle size.
Which Should You Choose?
Choose Axios If:
- You need upload progress tracking — Axios is the only option with built-in progress events
- Your project already uses it — migration cost isn't worth the bundle savings
- You need the most community resources — tutorials, Stack Overflow answers, examples
Choose ky If:
- You're building a browser-only application
- Bundle size matters — 4x smaller than Axios
- You want a modern, fetch-based API
- You appreciate built-in retries with exponential backoff
Choose ofetch If:
- You need a universal client (browser + Node.js + edge)
- You're using Nuxt, Nitro, or the UnJS ecosystem
- Bundle size matters — smallest option
- You want the simplest possible API
Choose Native fetch If:
- Your requests are simple (no retries, interceptors, or complex error handling)
- You want zero dependencies
- You're comfortable writing a thin wrapper
Migration: Axios to ky
// Before (Axios)
const { data } = await axios.get<User[]>('/api/users');
const { data: user } = await axios.post<User>('/api/users', { name: 'Alice' });
// After (ky)
const data = await ky.get('/api/users').json<User[]>();
const user = await ky.post('/api/users', { json: { name: 'Alice' } }).json<User>();
The migration is straightforward. The main API differences are:
kychains.json()instead of destructuring{ data }- POST body uses
{ json: ... }instead of a direct second argument - Interceptors become hooks
Our Recommendation
For new projects: ofetch. It's the smallest, runs everywhere, and has the cleanest API. If you're building browser-only, ky is equally good.
For existing Axios projects: Don't migrate unless bundle size is a pressing concern. Axios works fine — the 10KB difference isn't worth the refactoring cost for most projects.
Compare all HTTP client libraries on PkgPulse.
See the live comparison
View axios vs. ky on PkgPulse →