<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/axios-alternatives-2026-got-ky-undici -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/axios-alternatives-2026-got-ky-undici/raw.md -->
<!-- Source path: content/guides/axios-alternatives-2026-got-ky-undici.mdx -->

---
title: "axios Alternatives 2026: got, ky, and undici"
description: "axios was compromised in a supply chain attack in March 2026. Compare got, ky, undici, and native fetch — performance, bundle size, and TypeScript support."
date: "2026-04-03"
author: "PkgPulse Team"
tier: 2
tags: ["axios", "got", "ky", "undici", "http-client", "security", "supply-chain", "nodejs", "2026"]
og_image: "/images/guides/axios-alternatives-2026-got-ky-undici.webp"
---

## TL;DR

**On March 31, 2026, the axios npm package was backdoored by North Korean threat actor UNC1069 — versions 1.14.1 and 0.30.4 delivered a cross-platform Remote Access Trojan to any project that installed them.** The safe axios versions are 1.14.0 and 0.30.3. If you're evaluating whether to stay on axios or migrate, here's the current state of Node.js HTTP clients: **got** for feature-complete Node.js API work, **ky** for browser/isomorphic apps, **undici** for maximum throughput, and **native fetch** if you need zero dependencies.

## Key Takeaways

- **Compromised versions**: axios@1.14.1 (latest tag) and axios@0.30.4 (legacy tag) — both contain WAVESHAPER.V2 backdoor
- **Safe versions**: axios@1.14.0 and axios@0.30.3 — roll back if you pulled latest between March 31 00:21–03:25 UTC
- **got**: 26M weekly downloads, feature-rich, Node.js-only, ESM-first, built-in TypeScript, retries and hooks
- **ky**: 2M weekly downloads, 9.12 kB bundle, no dependencies, browser + Node.js, modern Fetch wrapper
- **undici**: 26M weekly downloads, 18,340 req/sec raw throughput, powers Node.js built-in `fetch`, HTTP/2
- **native fetch**: zero install, built into Node.js 18+, good enough for most straightforward use cases

---

## The March 2026 axios Supply Chain Attack

At 00:21 UTC on March 31, 2026, an attacker who had compromised the npm credentials of axios lead maintainer `jasonsaayman` published two backdoored releases simultaneously:

- **axios@1.14.1** — pushed as the `latest` tag, affecting anyone running `npm install axios` or automated dependency updates
- **axios@0.30.4** — pushed as the `legacy` tag, targeting projects on the older 0.x branch

The malicious packages introduced a hidden dependency called `plain-crypto-js`, which contained an obfuscated postinstall script. The script executed immediately on installation — before any application code ran — and deployed **WAVESHAPER.V2**, a cross-platform backdoor attributed to UNC1069, a financially motivated North Korean threat actor active since at least 2018.

npm removed the compromised packages at 03:25 UTC, three hours after they appeared. Elastic Security Labs, Datadog Security Labs, and Google's GTIG published detailed analyses confirming the attribution and payload behavior. The WAVESHAPER.V2 backdoor exfiltrates cloud access keys, database passwords, and API tokens, and installs a persistent Remote Access Trojan on Windows, macOS, and Linux.

**What this means for your project:**

- If your lockfile pins `axios@1.14.1` or `axios@0.30.4`, treat any system that ran `npm install` between March 31 00:21 and 03:25 UTC as fully compromised.
- If your lockfile pins `axios@1.14.0` or earlier, you were not affected by this specific incident.
- Projects using got, ky, undici, or native fetch were not impacted.

The attack has practical security implications beyond this single incident: axios's architecture requires a `postinstall` hook for browser compatibility shims, making it structurally dependent on a feature that supply chain attacks repeatedly exploit. Alternative libraries — ky, got, undici — carry no `postinstall` hooks at all.

---

## Quick Comparison

| | axios | got | ky | undici | native fetch |
|---|---|---|---|---|---|
| Weekly Downloads | 100M | 26M | 2M | 26M | built-in |
| GitHub Stars | 108K | 14.7K | 13.8K | 7.1K | — |
| Bundle Size (min+gzip) | ~13 kB | ~48 kB | 3.23 kB | varies | 0 |
| Browser Support | ✅ | ❌ | ✅ | ❌ | ✅ |
| TypeScript | bundled | bundled | bundled | bundled | native |
| postinstall hooks | ✅ | ❌ | ❌ | ❌ | ❌ |
| HTTP/2 | ❌ | ✅ | ❌ | ✅ | partial |
| Built-in Retries | ❌ | ✅ | ✅ | ❌ | ❌ |
| License | MIT | MIT | MIT | MIT | — |

---

## got: The Feature-Complete Node.js Client

got is the go-to axios alternative when you need a rich feature set and are writing Node.js-only code. Version 14.x is ESM-first with full TypeScript definitions bundled — no `@types/got` required.

```bash
npm install got
```

```typescript
import got from 'got';

// Basic GET with automatic JSON parsing
const data = await got('https://api.example.com/users').json();

// POST with JSON body — got handles Content-Type automatically
const result = await got.post('https://api.example.com/users', {
  json: { name: 'Alice', email: 'alice@example.com' },
}).json();

// Retry on failure (default: 2 retries with exponential backoff)
const withRetry = await got('https://api.example.com/data', {
  retry: {
    limit: 3,
    methods: ['GET'],
    statusCodes: [429, 500, 502, 503],
  },
}).json();

// Timeout configuration
const withTimeout = await got('https://api.example.com/slow', {
  timeout: {
    request: 5000,    // 5s total
    connect: 1000,    // 1s to establish connection
    response: 3000,   // 3s for first byte
  },
}).json();
```

got's killer feature is its **hooks system**, which replaces axios interceptors with a more composable pattern:

```typescript
import got, { type Got } from 'got';

// Create a reusable instance (equivalent to axios.create())
const api: Got = got.extend({
  prefixUrl: 'https://api.example.com',
  headers: { 'Authorization': `Bearer ${process.env.API_TOKEN}` },
  hooks: {
    beforeRequest: [
      (options) => {
        console.log(`→ ${options.method} ${options.url}`);
      },
    ],
    afterResponse: [
      (response) => {
        console.log(`← ${response.statusCode} ${response.url}`);
        return response;
      },
    ],
    beforeError: [
      (error) => {
        console.error(`Request failed: ${error.message}`);
        return error;
      },
    ],
  },
});

// Use the instance — prefixUrl + relative path
const users = await api.get('users').json();
const user = await api.post('users', { json: { name: 'Bob' } }).json();
```

got also supports **streaming** natively, which is where it pulls ahead of axios for server-side use:

```typescript
import got from 'got';
import { createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';

// Stream a large response to disk without buffering the whole response
await pipeline(
  got.stream('https://example.com/large-file.zip'),
  createWriteStream('./output.zip'),
);
```

**When to use got**: Node.js-only services, complex APIs needing retries and hooks, streaming downloads/uploads, HTTP/2 endpoints, projects where you want a direct axios migration path.

---

## ky: The Modern Fetch Wrapper

ky wraps the native Fetch API with a developer-friendly interface. At 9.12 kB minified (3.23 kB gzip) with zero dependencies, it's the lightest full-featured option and runs identically in browsers, Node.js 18+, Deno, and Bun.

```bash
npm install ky
```

```typescript
import ky from 'ky';

// GET with JSON — no .data wrapper, just the parsed object
const user = await ky.get('https://api.example.com/users/1').json();

// POST with automatic Content-Type: application/json
const created = await ky.post('https://api.example.com/users', {
  json: { name: 'Alice' },
}).json();

// Built-in retry with exponential backoff
const resilient = await ky.get('https://api.example.com/flaky', {
  retry: {
    limit: 3,
    methods: ['get', 'head'],
    statusCodes: [429, 503],
    backoffLimit: 3000,
  },
}).json();

// Timeout (throws on expiry)
const timed = await ky.get('https://api.example.com/slow', {
  timeout: 5000,
}).json();
```

ky's `beforeRequest`/`afterResponse` hooks work similarly to got's but are designed around the Fetch API's `Request`/`Response` objects:

```typescript
import ky from 'ky';

const api = ky.create({
  prefixUrl: 'https://api.example.com',
  hooks: {
    beforeRequest: [
      (request) => {
        request.headers.set('Authorization', `Bearer ${process.env.API_TOKEN}`);
      },
    ],
    afterResponse: [
      async (_request, _options, response) => {
        if (response.status === 401) {
          // Refresh token and retry
          const newToken = await refreshToken();
          const retryRequest = new Request(response.url, {
            headers: { Authorization: `Bearer ${newToken}` },
          });
          return ky(retryRequest);
        }
        return response;
      },
    ],
  },
});
```

Because ky is built on `fetch`, it works in Service Workers and Edge Runtimes (Cloudflare Workers, Vercel Edge Functions, Deno Deploy) where Node.js-specific APIs like `http.request` are unavailable. This is ky's primary advantage over got.

**When to use ky**: Browser-side code, Next.js apps that share fetch logic between client and server, edge runtimes, Deno/Bun projects, anywhere bundle size matters.

For a deeper ky vs. axios breakdown, see [Axios vs ky in 2026](/guides/axios-vs-ky-2026).

---

## undici: Maximum Throughput

undici is the HTTP/1.1 (and now HTTP/2) client that powers Node.js's built-in `fetch`. If you import `fetch` in Node.js 18+, you're already using undici under the hood. Installing it as a direct dependency gives you access to lower-level APIs and newer undici versions than what ships with your Node.js install.

```bash
npm install undici
```

undici benchmarks consistently show the highest raw throughput of any Node.js HTTP client:

```
50 TCP connections, pipelining depth 10, Node 22.x:

undici request:   18,340 req/sec
undici stream:    18,245 req/sec
undici pipeline:  13,364 req/sec
undici fetch:      5,903 req/sec   ← same as Node.js built-in fetch
got:               ~5,000 req/sec
axios:             ~4,800 req/sec
```

The `request` and `stream` APIs provide this throughput by bypassing the `fetch` Response abstraction:

```typescript
import { request } from 'undici';

// High-throughput request — no fetch overhead
const { statusCode, headers, body } = await request(
  'https://api.example.com/data',
  { method: 'GET' }
);

const data = await body.json();

// Connection pooling (default behavior — reuses TCP connections)
import { Pool } from 'undici';

const pool = new Pool('https://api.example.com', {
  connections: 20,
  pipelining: 4,
});

// Batch requests using the pool
const responses = await Promise.all([
  pool.request({ path: '/users', method: 'GET' }),
  pool.request({ path: '/products', method: 'GET' }),
  pool.request({ path: '/orders', method: 'GET' }),
]);
```

undici v6 (shipping with Node.js 22+) adds HTTP/2 multiplexing, which reduces latency significantly for API endpoints that accept concurrent requests over the same connection:

```typescript
import { Agent } from 'undici';

// Force HTTP/2 connections
const agent = new Agent({ allowH2: true });

const { body } = await request('https://api.example.com/data', {
  dispatcher: agent,
});
```

**When to use undici**: High-throughput servers making many outbound requests, microservices with strict latency requirements, cases where you need HTTP/2 connection pooling, proxy implementations.

For a focused undici vs got comparison, see [got vs undici vs node-fetch (2026)](/guides/got-vs-undici-vs-node-fetch-http-clients-nodejs-2026).

---

## Native fetch: When Zero Dependencies Win

Node.js 18 shipped with `fetch` as a global — no install required. For simple API calls, it handles the most common cases without adding any package to your dependency tree:

```typescript
// No imports needed in Node.js 18+
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}
const users = await response.json();

// POST with JSON
const created = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Alice' }),
});
```

The supply chain attack against axios highlighted what many developers had already observed: adding a third-party HTTP client means adding a trusted target for future attacks. For simple CRUD API consumption, native fetch eliminates that surface.

The tradeoffs are real: no built-in retries, no request hooks, no automatic JSON parsing of the request body, no timeout via `setTimeout` integration (use `AbortController` with `signal`), and verbose error handling. For anything beyond simple request/response, reach for ky or got.

**When to use native fetch**: Lambda functions and serverless environments where cold start matters, internal CLI tools, scripts where dependencies are a burden, projects that only need basic GET/POST without retry or timeout semantics.

---

## Migration: From axios to Alternatives

### axios → got (Node.js services)

```typescript
// Before: axios
import axios from 'axios';

const client = axios.create({
  baseURL: 'https://api.example.com',
  headers: { Authorization: `Bearer ${token}` },
  timeout: 5000,
});

const { data } = await client.get('/users');
const { data: created } = await client.post('/users', { name: 'Alice' });

// After: got
import got from 'got';

const client = got.extend({
  prefixUrl: 'https://api.example.com',
  headers: { Authorization: `Bearer ${token}` },
  timeout: { request: 5000 },
});

const data = await client.get('users').json();
const created = await client.post('users', { json: { name: 'Alice' } }).json();
```

### axios → ky (browser/isomorphic)

```typescript
// Before: axios
import axios from 'axios';

const { data } = await axios.get('https://api.example.com/users');
const { data: created } = await axios.post(
  'https://api.example.com/users',
  { name: 'Alice' }
);

// After: ky
import ky from 'ky';

const data = await ky.get('https://api.example.com/users').json();
const created = await ky.post('https://api.example.com/users', {
  json: { name: 'Alice' },
}).json();
```

The key migration difference: axios wraps responses in a `{ data }` envelope. Both got and ky call `.json()` directly on the response — there is no `response.data` wrapper.

### Interceptors → Hooks

```typescript
// Before: axios interceptors
client.interceptors.request.use((config) => {
  config.headers.Authorization = `Bearer ${getToken()}`;
  return config;
});

client.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) logout();
    return Promise.reject(error);
  }
);

// After: got hooks (same pattern, different API)
const client = got.extend({
  hooks: {
    beforeRequest: [(options) => {
      options.headers['Authorization'] = `Bearer ${getToken()}`;
    }],
    afterResponse: [(response) => {
      if (response.statusCode === 401) logout();
      return response;
    }],
  },
});
```

---

## When to Use Which

```
Still on axios?
  → Check your lockfile. axios@1.14.1 or @0.30.4? Rotate credentials, treat system as compromised.
  → Safe (1.14.0 or earlier)? axios remains viable — the maintainers responded quickly and added MFA.

New project / migration?
  → Need browser support or edge runtime? → ky
  → Node.js only, complex needs (retries, hooks, streaming, HTTP/2)? → got
  → High-throughput server, raw performance, connection pooling? → undici
  → Simple scripts, serverless, minimal dependencies? → native fetch

Browser-only React app?
  → ky or native fetch — axios's browser support is no longer a differentiator now that fetch is universal
```

The security argument for migration is real but shouldn't be overstated. The compromise window was three hours; projects that pin lockfile versions and use `npm audit` were protected. The deeper issue is structural: axios's postinstall hook gives supply chain attackers an automatic code execution primitive at install time. got, ky, and undici don't have that hook at all.

## When Axios Still Wins

The security incident is real and the advice to migrate is sound — but it would be dishonest to pretend axios has no genuine advantages. For teams that are not migrating immediately, understanding what axios does well clarifies what they would lose.

Axios interceptors are the most mature request/response middleware system in the JavaScript HTTP client ecosystem. The pattern of adding an interceptor to attach a bearer token, then another to refresh that token on 401 responses and retry the original request, has been implemented and debugged in production across thousands of codebases. got's hooks system covers the same use case but with a different API, and ky's `afterResponse` hooks require more careful implementation to avoid infinite retry loops on repeated 401s. The axios interceptor pattern is battle-tested and has extensive Stack Overflow documentation.

Request and response transformation — converting dates from ISO strings to `Date` objects on every response, normalizing API-specific error shapes, adding HMAC signatures to every outbound request body — is cleanly handled by axios transformers. The `transformRequest` and `transformResponse` arrays let you build a processing pipeline that applies globally across a client instance. This reduces boilerplate in individual API call sites. In got and ky, this same behavior requires hooks, which achieve the same result but with more explicit wiring.

The axios plugin ecosystem, while smaller than it once was, includes useful packages like `axios-retry` (automatic retry with exponential backoff), `axios-cache-adapter` (response caching with configurable TTLs), and `axios-mock-adapter` (test mocking). These packages are mature and well-tested. got has some equivalent functionality built-in (retries), and ky has retries built-in as well, but the adapter-based mocking ecosystem for testing is richer in axios.

The `baseURL` + instance model in axios is clean for API clients. `axios.create({ baseURL: 'https://api.example.com', timeout: 5000 })` produces an instance where every call automatically inherits those settings. The equivalent in got (`got.extend({ prefixUrl })`) and ky (`ky.create({ prefixUrl })`) works the same way, but axios's model has been documented and discussed more extensively, which matters for teams onboarding new developers.

---

## The Native Fetch Maturity Story

Node.js 18 shipped `fetch` as an experimental global, Node.js 21 made it stable, and Node.js 22 ships with undici v6 as the underlying implementation. The maturity trajectory has been fast. For the most common HTTP use cases — JSON GET/POST requests, setting headers, reading response bodies — native `fetch` works correctly and predictably in 2026.

What remains missing compared to axios or got: no automatic request body serialization. With axios and got, passing `json: { name: 'Alice' }` automatically sets `Content-Type: application/json` and serializes the body. With native fetch, you write `JSON.stringify(body)` and set the header manually. This is two lines instead of one option, but it is a friction point that ky and got eliminate.

Timeout handling with native fetch requires `AbortController` with `AbortSignal.timeout()`. Node.js 17.3+ ships this API, so for Node.js 20+ you write `fetch(url, { signal: AbortSignal.timeout(5000) })`. This is now ergonomic enough that it is not a meaningful disadvantage over axios's `timeout` option.

The edge runtime story is where native fetch has definitively won. Cloudflare Workers, Vercel Edge Functions, and Deno Deploy all implement the Fetch API specification. Code written with native fetch or ky (which wraps fetch) runs identically on these platforms. got and axios, which depend on Node.js's `http` module, do not. For teams building edge-deployed Next.js middleware or Cloudflare Workers, the choice of ky or native fetch is not optional — it is required by the runtime.

The missing piece for native fetch in production backend services is retry logic. Transient network failures, 503s from overloaded downstream services, and rate limit 429s all require retry with backoff. Implementing this on top of native fetch is straightforward but verbose. For backend services that make many outbound requests to third-party APIs, wrapping fetch with a simple retry function or reaching for ky (which has `retry` built-in) is the pragmatic choice.

---

View download trends for [axios vs got vs undici](/compare/axios-vs-got) on PkgPulse.
