← Back to blog
··14 min read

Axios vs ky vs ofetch 2026: Which HTTP Client Should You Use in JavaScript?

AxioskyofetchHTTP ClientFetchTypeScriptNode.jsNext.js
Axios vs ky vs ofetch 2026: Which HTTP Client Should You Use in JavaScript?

# Axios vs ky vs ofetch 2026: Which HTTP Client Should You Use in JavaScript?

Every app talks to an API, and for over a decade the reflex answer to "how do I make the request" was one word: Axios. In 2026 that reflex is worth questioning. The native Fetch API now ships in every browser and every serious runtime — Node, Bun, Deno, Cloudflare Workers — and a new generation of tiny wrappers gives you axios-style ergonomics on top of it for a fraction of the size. This guide compares the three clients actually worth reaching for today: Axios, the battle-tested incumbent; ky, Sindre Sorhus's elegant Fetch wrapper; and ofetch, the universal client that powers Nuxt and Nitro.

If you build or sell production-ready templates, the HTTP layer is one of those quiet decisions a buyer inherits forever — so it is worth getting right.

The 30-Second Answer

  • Axios — the full-featured incumbent (~50M weekly downloads, ~13 KB gzipped). Interceptors, upload/download progress, request cancellation, and a decade of documentation. Not built on native fetch, and its size and reach make it a large supply-chain surface. Reach for it when you need its ecosystem or already run it.
  • ky — a tiny (~4 KB gzipped), zero-dependency client built on fetch, from the author of dozens of ecosystem staples. Hooks (interceptors), automatic retries, timeouts, typed JSON, and — as of ky 2.0 — built-in response schema validation. The modern default for a new browser or full-stack app.
  • ofetch — the unjs universal client (well under 4 KB gzipped) with an axios-like API that runs *identically* in the browser, on the server, and at the edge. Auto-parses responses, throws on non-2xx, retries. The pick when you want one wrapper across every runtime, especially Nuxt/Nitro.
  • The Landscape: Three Answers to One Question

    The three clients are not really competing on the same axis. They are three answers to the question *"how much do I want between my code and the network, and where does that code run?"*

    Axios predates modern fetch. It shipped its own adapter — XMLHttpRequest in the browser, Node's http module on the server — so it never depended on the platform having a good HTTP primitive. That is why it works everywhere back to ancient environments, and why it carries features fetch lacks. It is also why it is heavier: you are shipping a whole HTTP stack.

    ky and ofetch take the opposite bet. Native fetch is now *everywhere that matters*, so both are thin wrappers that add ergonomics — retries, timeouts, throw-on-error, JSON handling, a hooks lifecycle — while inheriting fetch's streaming, AbortController cancellation, and edge compatibility for free. ky optimizes for a clean, browser-first API; ofetch optimizes for behaving the same in every runtime.

    Bundle Size and Downloads

    Size is the most visible difference, and it is not close.

    ClientGzipped sizeWeekly downloadsBuilt on
    **Axios**~13 KB~50MOwn XHR/http adapter
    **ky**~4 KB~7MNative fetch
    **ofetch**<4 KB~27MNative fetch

    Axios's download count reflects a decade of default status, not superiority; ofetch's high number is largely the Nuxt/Nitro ecosystem pulling it in transitively. For a browser bundle where every kilobyte on the critical path costs you, a 3x–4x size reduction from axios to a fetch-based client is real — especially on the kind of landing pages and dashboards where first load matters.

    Axios: The Incumbent

    Axios is the client everyone knows. Its API is comfortable, its interceptors are genuinely powerful, and there is an answer to every question you will ever have.

    ts
    import axios from "axios";
    
    const api = axios.create({
      baseURL: "https://api.example.com",
      timeout: 10000,
    });
    
    // Interceptor: attach auth on every request
    api.interceptors.request.use((config) => {
      const token = getToken();
      if (token) config.headers.Authorization = `Bearer ${token}`;
      return config;
    });
    
    // Interceptor: refresh token on 401, then retry
    api.interceptors.response.use(
      (res) => res,
      async (error) => {
        if (error.response?.status === 401) {
          await refreshToken();
          return api.request(error.config);
        }
        return Promise.reject(error);
      }
    );
    
    const { data } = await api.get("/users/1"); // data is already parsed JSON

    What you get: interceptors, automatic JSON transform, upload and download progress events (still axios's genuine standout — fetch's progress story is clumsier), request cancellation, timeouts, and a transform pipeline. What it costs: ~13 KB gzipped, an adapter layer instead of native fetch, and the reality that one of npm's most-installed packages is a high-value target. 2026 was a rough year for the npm supply chain, and a large, deeply-depended-on client is more to trust. If you stay on axios, pin exact versions, watch advisories, and read our companion deep-dive on Axios security vulnerabilities and how to fix them.

    Use axios when: you already run it, you need interceptor-based auth flows and upload progress, or you must support environments without native fetch.

    ky: The Elegant Fetch Wrapper

    ky is what fetch would look like if it had shipped with sensible defaults. It is about 4 KB gzipped, has zero dependencies, and runs natively in browsers, Node, Bun, Deno, Cloudflare Workers, and Web Workers.

    ts
    import ky from "ky";
    
    const api = ky.create({
      prefixUrl: "https://api.example.com",
      timeout: 10000,
      retry: { limit: 2 }, // automatic retries, honors Retry-After on 429
      hooks: {
        beforeRequest: [
          (request) => {
            const token = getToken();
            if (token) request.headers.set("Authorization", `Bearer ${token}`);
          },
        ],
        afterResponse: [
          async (request, options, response) => {
            if (response.status === 401) {
              await refreshToken();
              return ky(request); // retry with fresh token
            }
          },
        ],
      },
    });
    
    // Throws on non-2xx automatically; .json() is typed
    const user = await api.get("users/1").json<User>();

    What you get: hooks that do everything axios interceptors do (beforeRequest, afterResponse, beforeRetry, beforeError), automatic retries that read the Retry-After header on 429 Too Many Requests, timeouts, a typed .json() shortcut, and throw-on-error by default — no more forgetting to check response.ok. ky 2.0 (2026) consolidated the hook handling, improved timeout logic, and added built-in response schema validation, so you can validate a response against a schema library right in the request — a natural pairing with Zod, Yup, or Valibot.

    Use ky when: you are starting a new browser or full-stack app, you want the smallest modern client with great ergonomics, or you deploy to the edge and want zero adapter surprises.

    ofetch: The Universal Client

    ofetch comes from unjs, the toolkit behind Nuxt and Nitro, and its whole reason for existing is to behave identically everywhere — the same code in the browser, in a Node server, and in an edge function. It is under 4 KB gzipped and reads like axios.

    ts
    import { ofetch } from "ofetch";
    
    const api = ofetch.create({
      baseURL: "https://api.example.com",
      retry: 2,
      timeout: 10000,
      onRequest({ options }) {
        const token = getToken();
        if (token) options.headers.set("Authorization", `Bearer ${token}`);
      },
      onResponseError({ response }) {
        if (response.status === 401) refreshToken();
      },
    });
    
    // Auto-parses JSON, throws on non-2xx
    const user = await api<User>("/users/1");

    What you get: automatic response parsing (it detects JSON), throw-on-error with a helpful FetchError, retries, a request/response lifecycle (onRequest, onResponse, onRequestError, onResponseError), and true universality. What you give up relative to axios: there are no interceptors or plugins in the axios sense, and while TypeScript works, ky's typed .json() ergonomics are a touch sharper. Use ofetch when: you live in the Nuxt/Nitro world, you want one wrapper that is genuinely runtime-agnostic, or you value an axios-familiar API in a fetch-sized package.

    The Decision Table

    NeedBest pick
    Smallest modern client, new browser/full-stack app**ky**
    One wrapper identical across browser + server + edge**ofetch**
    Nuxt / Nitro project**ofetch**
    Interceptor-based auth ecosystem, already invested**Axios**
    Upload/download progress events**Axios**
    Response schema validation built in**ky 2.0**
    Edge runtimes (Workers, Vercel Edge, Deno, Bun)**ky** or **ofetch**
    A few trivial requests, zero depsNative **fetch**
    Smallest supply-chain surface**ky** (zero deps)

    Do You Even Need One?

    Be honest about the simple case. Native fetch is everywhere, and for a handful of requests, a five-line wrapper is fine and ships zero bytes. The catch is the boilerplate you *will* get subtly wrong: fetch does not reject on HTTP errors (a 500 is a resolved promise), has no retries or timeouts, does not serialize/parse JSON for you, and gives you no natural home for auth across every call. The moment you need retries, timeouts, centralized auth, or consistent error handling across many endpoints, a ~4 KB fetch-based client pays for itself. This is also why an HTTP client and a data-fetching/caching layer are different tools that live together — see TanStack Query vs SWR vs RTK Query for the layer that handles caching, revalidation, and request de-duplication on top of whichever client you pick.

    Migrating Off Axios

    Both ky and ofetch are close enough to axios that a migration is mechanical for most codebases:

  • axios.create({ baseURL }) → ky.create({ prefixUrl }) (note: ky's prefixUrl does not want a leading slash on the path) or ofetch.create({ baseURL }).
  • Interceptors → ky hooks or ofetch onRequest/onResponse.
  • response.data → ky's .json() or ofetch's direct return value (both auto-parse).
  • Error handling → both throw on non-2xx like axios does, so your try/catch structure survives; inspect error.response (ofetch) or the thrown HTTPError (ky) instead of axios's error shape.
  • Cancellation → pass a standard AbortController signal instead of axios's CancelToken.
  • Why This Matters if You Sell Templates

    A buyer opening your Next.js starter or React kit judges the HTTP layer in seconds. A tiny, native-fetch client with typed responses, sane retries, and one clean place to attach auth signals a maintained, modern codebase; a heavy adapter-based client with hand-rolled error checks signals the opposite. Picking a small, edge-ready client is exactly the kind of quiet quality that turns a browse into a sale — and it sits naturally beside the stacks buyers already come to CodeCudos for. Make sure the rest of the stack pulls its weight too: see the best tech stack for web apps in 2026 and pick the runtime and API framework it runs on.

    The Bottom Line

    All three put a response on the promise — but they come at it from different eras and priorities, and matching the tool to *where your code runs* is the whole decision.

  • Axios — the incumbent: complete, comfortable, and everywhere, at the cost of size and a large supply-chain surface. The choice when you need its ecosystem or already run it.
  • ky — the modern default: tiny, zero-dependency, native to every runtime, with hooks, retries, and schema validation in 2.0. The choice for a new browser or full-stack app.
  • ofetch — the universal one: an axios-like API in a fetch-sized package that behaves identically across browser, server, and edge. The choice for Nuxt/Nitro and runtime-agnostic code.
  • Reach for ky when you are starting fresh and want the smallest elegant client, ofetch when universality across runtimes is the priority, and axios when its interceptor ecosystem or upload progress genuinely earns its weight — and remember that native fetch alone is a legitimate answer for the simplest apps.

    Ready to turn what you build into income? List your Next.js or React template on CodeCudos, lock down your dependencies with our Axios security guide, and make sure the whole thing reads as production-ready.

    Frequently asked questions

    What is the core difference between Axios, ky, and ofetch?▾

    The core difference is what each one is built on and how much it carries. Axios is a standalone HTTP library with its own adapter layer — historically XMLHttpRequest in the browser and Node's http module on the server — so it does not depend on the platform's fetch and brings its own feature set: interceptors, automatic JSON transform, upload/download progress, request cancellation, and broad legacy-environment support. That completeness is why it is the default in millions of projects, but it also means about 13 KB gzipped and a larger surface to trust. ky and ofetch are both thin wrappers around the native Fetch API that is now available in every modern runtime, so they are far smaller (roughly 4 KB and under 4 KB gzipped respectively) and inherit fetch's streaming, AbortController cancellation, and edge compatibility for free. ky, from Sindre Sorhus, focuses on a clean browser-first ergonomic API with hooks, retries, and timeouts and zero dependencies; ofetch, from the unjs team behind Nuxt and Nitro, focuses on being universal — the same wrapper behaving identically in the browser, Node, and edge runtimes, with an axios-like ergonomic layer (auto-parsed responses, throw-on-error, retries). In one line: axios is a full-featured client that predates modern fetch, while ky and ofetch are lightweight, modern wrappers on top of fetch, ky optimising for elegance and ofetch for universality.

    Is Axios still worth using in 2026, or should I switch?▾

    Axios is still perfectly usable and, for existing code, usually not worth ripping out just to save a few kilobytes. It remains the most documented HTTP client in the ecosystem, its interceptor model is genuinely useful for cross-cutting concerns like auth-token refresh and centralized error handling, and it works in older environments where native fetch is unavailable. The reasons to reconsider it for new projects are size and supply-chain surface. At around 13 KB gzipped it is several times larger than ky or ofetch, it is not built on the platform's own fetch, and as one of the most-installed packages on npm it is a high-value target — 2026 saw serious supply-chain incidents across the npm ecosystem, and a smaller client with zero or few dependencies is simply less to trust and pin. If you are starting fresh and do not specifically need axios's ecosystem, a fetch-based client like ky or ofetch is the more modern default. If you are staying on axios, treat it like any critical dependency: pin exact versions, watch advisories, and review our companion guide on Axios security vulnerabilities and how to fix them. The honest rule is that axios is a fine, safe choice when you are already invested or need its features, and a fetch-based client is the better greenfield pick.

    How do ky's hooks compare to Axios interceptors?▾

    They solve the same problem — running code before a request goes out or after a response comes back — with slightly different shapes. Axios exposes interceptors through axios.interceptors.request.use() and axios.interceptors.response.use(), where each interceptor receives and returns the config or response and they run in a chain; this is where most apps attach an Authorization header, refresh expired tokens, or normalize errors. ky exposes the same capability through a hooks option you pass when creating an instance: beforeRequest hooks let you mutate the outgoing Request (adding headers, logging), afterResponse hooks let you inspect or replace the Response (including retrying after a token refresh), and beforeRetry and beforeError hooks give you finer control over ky's built-in retry and error flow. The practical differences are that ky's hooks operate on standard Request and Response objects rather than an axios-specific config, which makes them portable across fetch-based tooling, and that ky bakes in retries and timeouts so you often need fewer hooks than the equivalent axios setup. ofetch offers a similar lifecycle through onRequest, onResponse, onRequestError, and onResponseError callbacks. So all three support the interceptor pattern; ky and ofetch just express it against the native fetch primitives.

    Which HTTP client works best on the edge — Cloudflare Workers, Vercel Edge, Deno, and Bun?▾

    The fetch-based clients, ky and ofetch, are the better fit for edge and alternative runtimes, because those runtimes ship a native fetch and standard Web APIs but often do not provide Node's http module or XMLHttpRequest. ky is explicitly built for this world — it runs in browsers, Node, Bun, Deno, Cloudflare Workers, and Web Workers with no adapter or polyfill — and ofetch is designed to run identically across the browser, Node, and edge runtimes, which is exactly why the Nuxt and Nitro ecosystem uses it as its universal client. Axios can run in these environments too, and modern versions have improved compatibility, but because it historically relied on its own XHR and Node adapters rather than native fetch, it has been more prone to bundle-size and compatibility friction in edge contexts. If your code is deployed to Cloudflare Workers or Vercel Edge functions, or you are targeting Deno and Bun as well as Node, defaulting to a fetch-based client removes a whole category of runtime surprises. See our guide on Cloudflare Workers vs AWS Lambda vs Vercel Functions for where that code actually runs, and our Bun vs Node vs Deno comparison for the runtime picture.

    Do I even need an HTTP-client library now that fetch is everywhere?▾

    Not always — and that is the honest answer. The native Fetch API is now available in every browser and every modern JavaScript runtime, and for a handful of simple GET and POST calls, plain fetch with a small helper is entirely reasonable and adds zero bytes. What the libraries buy you is the boilerplate you would otherwise write yourself and get subtly wrong: fetch does not reject on HTTP error statuses (a 404 or 500 is still a resolved promise, so you must check response.ok every time), it has no built-in retries or timeouts, it does not automatically serialize and parse JSON or set the right Content-Type, and it has no first-class place to attach auth headers or refresh tokens across every call. ky and ofetch are thin enough (about 4 KB and under 4 KB gzipped) that they give you throw-on-error, retries, timeouts, typed JSON, and a hooks/interceptor lifecycle for almost no cost, which is why they are popular even though fetch exists. The rule of thumb: if you are making a few trivial requests, native fetch with a tiny wrapper is fine; once you need retries, timeouts, centralized auth, or consistent error handling across many endpoints, a small fetch-based client pays for itself immediately, and only reach for axios when you specifically need its broader feature set or ecosystem.

    Related guides

    Browse Quality-Scored Code

    Every listing on CodeCudos is analyzed for code quality, security, and documentation. Find production-ready components, templates, and apps — or sell your own code and keep 90%.

    Browse Marketplace →