jump to code

“ky vs axios”

Do You Really Need Another HTTP Client?

You might not need any additional HTTP client. The native fetch API, built into modern browsers and Node.js, is powerful and capable of handling many common use cases. If you’re making simple HTTP requests, fetch might be all you need.

However, real-world applications often require more sophisticated features. You might need to:

  • Create reusable API instances with predefined base URLs and authentication
  • Implement request/response interceptors for token refresh flows
  • Transform request/response data consistently across your application
  • Track file upload/download progress
  • Handle retries for failed requests
  • Manage request cancellation
  • Show toast notifications for various HTTP responses

This is where Ky.js shines.

What is Ky.js?

Ky.js is a modern, elegant HTTP client built on top of the Fetch API. Think of it as a lightweight wrapper that adds powerful features while maintaining the simplicity and familiarity of fetch. In contrast, Axios is built on the older XMLHttpRequest technology.

Core Features

  • Lightweight: Only 4KB minzipped (compared to Axios’s 14KB)
  • Zero dependencies: Built directly on the Fetch API
  • Modern: Supports latest browsers, Node.js 18+, Bun, and Deno
  • TypeScript ready: Built-in TypeScript support
  • Interceptors: Support for request and response interceptors.
  • Api instance with base URL, authorization headers etc.
  • Error Handling, Retries, File upload progress
  • …And much more.

Installation

npm i ky

1. Simple Requests

GET Request

const user = await ky("/api/user").json();

POST Request

  • JSON Body
const json = await ky.post("/api/user", { json: { foo: "bar" } }).json();
  • FormData
const response = await ky.post("/api/user", { body: formData }).json();

2. Set Request Headers

const json = await ky
  .post("https://example.com", {
    headers: {
      "content-type": "application/json",
      Authorization: "Bearer token",
    },
    json: {
      foo: true,
    },
  })
  .json();

3. Reusable api instance

// With prefix URL
const api = ky.create({ prefixUrl: 'http://localhost:5000' });

// With authentication
const authenticatedAPI = ky.create({
prefixUrl: "http://localhost:5000",
headers: {
	Authorization: `Bearer ${Cookies.get("accessToken")}`,
	}
});

// usage
const user = await authenticatedAPI.get('/api/user').json();
const json = await authenticatedAPI.post('/api/user',{json:jsonData}).json());

4. Query params

const response = await ky
  .get("https://api.example.com/users", {
    searchParams: {
      page: 1,
      limit: 10,
      sort: "desc",
    },
  })
  .json();

5. Interceptors/Hooks

// Request Interceptor
const api = ky.extend({
  hooks: {
    beforeRequest: [
      (request) => {
        request.headers.set("X-Requested-With", "ky");
      },
    ],
  },
});

// Response Interceptor
const api = ky.extend({
  hooks: {
    afterResponse: [
      (request, options, response) => {
        if (response.status === 500) {
          toast.error("Internal Server Error");
        }
      },
    ],
  },
});

6. Retries

// Simple retry
const response = await ky("https://api.example.com", {
  retry: 5,
});

// Advanced retry configuration
const api = ky.create({
  retry: {
    limit: 3,
    methods: ["get", "put"],
    statusCodes: [408, 413, 429, 500, 502, 503, 504],
    afterStatusCodes: [413, 429, 503],
    maxRetryAfter: 5000,
    backoffLimit: 3000,
  },
  hooks: {
    beforeRetry: [
      async ({ request, options, error, retryCount }) => {
        console.log(`Retrying request (${retryCount} attempt)`);
        request.headers.set("Authorization", await getNewToken());
      },
    ],
  },
});

7. Request Cancellation

const controller = new AbortController();
const { signal } = controller;

// Cancel after 5 seconds
setTimeout(() => {
  controller.abort();
}, 5000);

try {
  const response = await ky("https://api.example.com/longrunning", {
    signal,
    timeout: 10000, // 10 second timeout
  }).json();
} catch (error) {
  if (error.name === "AbortError") {
    console.log("Request was cancelled");
  }
}

// Multiple request cancellation
const requests = [
  ky.get("https://api1.example.com", { signal }),
  ky.get("https://api2.example.com", { signal }),
  ky.get("https://api3.example.com", { signal }),
];

try {
  const responses = await Promise.all(requests);
} catch (error) {
  if (error.name === "AbortError") {
    console.log("All requests were cancelled");
  }
}

8. File Upload and Progress Tracking

const controller = new AbortController();
const { signal } = controller;

// Cancel after 5 seconds
setTimeout(() => {
  controller.abort();
}, 5000);

try {
  const response = await ky("https://api.example.com/longrunning", {
    signal,
    timeout: 10000, // 10 second timeout
  }).json();
} catch (error) {
  if (error.name === "AbortError") {
    console.log("Request was cancelled");
  }
}

// Multiple request cancellation
const requests = [
  ky.get("https://api1.example.com", { signal }),
  ky.get("https://api2.example.com", { signal }),
  ky.get("https://api3.example.com", { signal }),
];

try {
  const responses = await Promise.all(requests);
} catch (error) {
  if (error.name === "AbortError") {
    console.log("All requests were cancelled");
  }
}

9. Error Handling

try {
  const response = await ky
    .post("https://api.example.com/data", {
      json: { foo: "bar" },
    })
    .json();
} catch (error) {
  if (error.name === "HTTPError") {
    const errorJson = await error.response.json();
    console.log("Status:", error.response.status);
  } else if (error.name === "TimeoutError") {
    console.log("Request timed out");
  }
}

// Custom error handling with hooks
const api = ky.create({
  hooks: {
    beforeError: [
      (error) => {
        const { response } = error;
        if (response && response.body) {
          error.name = "CustomAPIError";
          error.message = `${response.body.message} (${response.status})`;
        }
        return error;
      },
    ],
  },
});

10. Typescript Support

interface User {
  id: number;
  name: string;
  email: string;
}

interface CreateUserDto {
  name: string;
  email: string;
}

// GET with type
const user = await api.get<User>(`users/123`).json();
// Alternative syntax
const user = await api.get(`users/123`).json<User>();

// POST with type
const newUser = await api
  .post<User>("users", {
    json: {
      name: "John Doe",
      email: "[email protected]",
    } as CreateUserDto,
  })
  .json();

11. Response Types

// JSON response
const jsonData = await ky.get("endpoint").json();

// Text response
const textResponse = await ky.get("endpoint").text();

// Blob response
const blobResponse = await ky.get("files/image.jpg").blob();

// ArrayBuffer response
const bufferResponse = await ky.get("files/document.pdf").arrayBuffer();

// Raw response
const rawResponse = await ky.get("endpoint");
const headers = Object.fromEntries(rawResponse.headers);
const status = rawResponse.status;

Comparison with Axios

FeatureKyAxios
Base ImplementationBuilt on Fetch APIBuilt on XMLHttpRequest
SizeSmaller (~4KB minzipped)Larger (~14KB minzipped)
DependenciesZero dependenciesHas dependencies
Browser SupportModern browsers onlyWider browser support
Node.js SupportNode.js 18+ (native fetch)All Node.js versions
Response ParsingManual (.json(), .text())Automatic based on content-type
Request BodyRequires manual stringification for JSONAutomatic transformation
InterceptorsUses hooks systemUses interceptors system
ProgressBoth upload and downloadBoth upload and download
TimeoutSimple timeout optionRequest and response timeouts
CancellationNative AbortControllerCustom Cancel Token
Request ConfigMore minimal APIMore extensive configuration options
TransformsHooks for request/responseData/Header transformers
Default SettingsVia .extend() and .create()Via defaults and instance creation
TypeScript SupportBuilt-inBuilt-in
Error HandlingHTTPError with response objectDetailed error object with config
Form DataNative FormData supportAutomatic handling
Request RetryBuilt-in retry with optionsRequires separate package
HTTP/2 SupportVia Fetch APINo direct support
Bundle Size ImpactSmaller footprintLarger footprint