Skip to main content

API Client Libraries: Axios vs ky vs ofetch in 2026

·PkgPulse Team

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

LibraryWeekly DownloadsSize (gzip)Built On
Axios50M+13KBXMLHttpRequest + Node adapter
ky2M3.3KBNative fetch
ofetch8M3KBNative 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-universal or 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

FeatureAxioskyofetch
Size (gzip)13KB3.3KB3KB
Built onXHR + adaptersfetchfetch
Browser
Node.js
Edge runtimes
Auto JSON
RetriesPlugin✅ Built-in✅ Built-in
Interceptors✅ (rich)Hooks
Upload progress
Timeout
TypeScript
AbortController
Streaming

Performance Comparison

For 1,000 sequential API calls:

LibraryTotal TimeBundle Cost
Native fetch8.2s0KB
Axios8.5s13KB
ky8.3s3.3KB
ofetch8.2s3KB

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:

  1. ky chains .json() instead of destructuring { data }
  2. POST body uses { json: ... } instead of a direct second argument
  3. 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 →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.