Skip to main content

axios Alternatives 2026: got, ky, and undici

·PkgPulse Team
0

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

axiosgotkyundicinative fetch
Weekly Downloads100M26M2M26Mbuilt-in
GitHub Stars108K14.7K13.8K7.1K
Bundle Size (min+gzip)~13 kB~48 kB3.23 kBvaries0
Browser Support
TypeScriptbundledbundledbundledbundlednative
postinstall hooks
HTTP/2partial
Built-in Retries
LicenseMITMITMITMIT

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.

npm install got
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:

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:

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.

npm install ky
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:

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.


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.

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:

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:

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).


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:

// 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)

// 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)

// 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

// 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.

View download trends for axios vs got vs undici on PkgPulse.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.