mrkeyoor.com_
Sat 08 Aug 17:40 UTC
npmWeb Frontendupdated 08 Aug 2026

ky

Ky is a dependency-free HTTP client built on the standard Fetch API for modern browsers, Node.js, Bun, and Deno. It keeps Request, Response, Headers, AbortSignal, and fetch options, then adds method shortcuts, automatic JSON bodies, non-2xx errors, retry and timeout policy, hooks, progress callbacks, configured instances, and typed or schema-validated JSON helpers. It is a convenience layer, not a separate transport stack.

Verdict

Ky is the nicest upgrade from bare fetch when its Node 22 and modern-browser floor matches your project. Skip it for simple calls, CommonJS services, or large streaming uploads where its retry machinery is a liability.

API stability3/5The central fetch-shaped call, method shortcuts, instances, and hooks remain recognizable, but major version 2 raises the Node floor to 22 and current documentation includes newer option names and lifecycle state objects that differ from older examples. The package is small enough to migrate, yet teams should treat major upgrades as code changes rather than passive dependency refreshes.
Docs5/5The README is a detailed reference covering every option, default retry behavior, timeout scope, hook order and return rules, error subclasses, progress, streams, proxying, HTTP/2, SSE, pagination, SSR, testing, and security cautions around credentials. It also calls out subtle costs such as retry buffering and empty-body JSON errors with direct examples.
Maintenance5/5npm serves version 2.0.2, the repository was pushed in July 2026, is not archived, and GitHub currently shows zero open issues and pull requests. The project has active current-version documentation, a modern runtime policy, and a focused dependency-free codebase that reduces supply-chain and transitive maintenance work.
Ecosystem5/5Ky recorded 7,194,934 npm downloads in the fetched week and has 17,015 GitHub stars. It works with the standard Fetch API types across browsers, Node, Bun, and Deno, integrates with Standard Schema validators, and accepts custom fetch implementations. Its ecosystem is compatibility through web standards rather than a large adapter plugin catalog.

Use it if

  • You like fetch semantics but want retries, timeouts, hooks, JSON shortcuts, and non-2xx errors in one small client
  • You need the same dependency-free request wrapper in modern browsers and Node 22 or newer
  • You want TypeScript JSON results to default to unknown and optionally validate responses with a Standard Schema validator
  • You need reusable client instances with base URL, headers, retry policy, instrumentation, or authentication hooks
Skip it if

Setup reality

`npm install ky` brings no dependencies or peer dependencies, and TypeScript declarations are included. The important gate is runtime: Ky 2.0.2 declares Node 22 or newer and is an ESM package. Modern browsers, Bun, and Deno are supported because they provide Fetch API primitives; older browsers and CommonJS-only Node applications are not the target. Relative URLs work in a browser, but server-side calls need an absolute destination or a suitable `baseUrl`. Include a trailing slash when a base URL contains a path, because normal URL resolution otherwise replaces the final path segment. Ky changes fetch defaults in ways that deserve an explicit review. It throws on non-2xx responses, retries selected methods and status codes with a default limit of two, and applies a per-attempt timeout; timeout failures are not retried unless `retryOnTimeout` is enabled. Add `totalTimeout` if retry delays must fit an overall deadline. Request hooks receive normalized Request objects and run at specific lifecycle points, so old hook signatures from Ky 0.x examples are wrong for version 2. JSON response generics only tell TypeScript what you expect; they do not validate the wire data unless you pass a Standard Schema validator. Browser CORS rules remain unchanged. For streaming uploads, retries clone the body with `tee()` and buffer it in memory, so set the retry limit to zero. Finally, an HTTPError holds the response and parsed data, while network and timeout failures use different error classes; handle them separately rather than assuming every exception has a status.

Patterns

Fetch typed JSONget-json

import ky from 'ky';

type User = {id: string; name: string};
const user = await ky.get('https://api.example.com/users/42').json<User>();

The generic is a compile-time assertion, not runtime validation. Without it, json returns unknown.

POST a JSON bodypost-json

import ky from 'ky';

const created = await ky.post('https://api.example.com/users', {
  json: {name: 'Ada'},
}).json<{id: string}>();

The json option stringifies the value and sets the content type. The json shortcut throws when the response body is empty.

Create a configured API clientcreate-client

import ky from 'ky';

export const api = ky.create({
  baseUrl: 'https://api.example.com/v1/',
  timeout: 10_000,
  totalTimeout: 30_000,
  headers: {'X-Client': 'dashboard'},
});

const users = await api.get('users').json();

Keep the trailing slash on a base URL with a path so URL resolution appends users instead of replacing v1.

Serialize query parameterssend-query-params

const results = await api.get('search', {
  searchParams: {q: 'open source', page: 2, archived: false},
}).json();

searchParams uses URLSearchParams semantics; undefined handling and array formats should be made explicit for APIs with custom conventions.

Attach authentication in a request hookattach-auth-header

const authedApi = api.extend({
  hooks: {
    beforeRequest: [({request}) => {
      const token = getAccessToken();
      if (token) request.headers.set('Authorization', `Bearer ${token}`);
    }],
  },
});

Ky 2 hooks receive a state object. beforeRequest runs once before retry handling, so refresh-per-retry logic belongs in beforeRetry or afterResponse.

Bound retries with jitter and a total timeoutconfigure-retries

const data = await api.get('reports', {
  retry: {
    limit: 3,
    statusCodes: [408, 429, 500, 502, 503, 504],
    jitter: true,
    retryOnTimeout: true,
  },
  timeout: 5_000,
  totalTimeout: 20_000,
}).json();

timeout is per attempt; totalTimeout covers retry delays and all attempts. Retrying non-idempotent work can duplicate side effects.

Upload a stream without retry bufferingdisable-stream-retries

await ky.post('https://api.example.com/import', {
  body: readableStream,
  retry: {limit: 0},
  headers: {'content-type': 'application/octet-stream'},
});

The README warns that enabled retries tee and buffer the whole ReadableStream in memory.

Read a structured HTTP errorhandle-http-error

import ky, {isHTTPError} from 'ky';

try {
  await api.get('users/missing').json();
} catch (error) {
  if (isHTTPError(error)) {
    console.error(error.response.status, error.data);
  } else {
    throw error;
  }
}

NetworkError and TimeoutError do not have an HTTP response; use the provided type guards rather than reading status unconditionally.

Inspect non-2xx responses without throwingallow-non-2xx

const response = await ky.get('https://api.example.com/health', {
  throwHttpErrors: false,
});

if (!response.ok) console.error(response.status);

This restores fetch-like status handling for HTTP responses; network and timeout errors can still throw.

Cancel with AbortControllercancel-request

const controller = new AbortController();
const request = api.get('slow-report', {signal: controller.signal}).json();

controller.abort();
await request;

The promise rejects after abort. Catch the error when cancellation is an expected user action.

Validate JSON with a Standard Schemavalidate-response

import {z} from 'zod';

const userSchema = z.object({id: z.string(), name: z.string()});
const user = await api.get('users/42').json(userSchema);

A schema mismatch throws SchemaValidationError. Use a validator version that implements Standard Schema, such as the version range named in Ky's README.

Track download progresstrack-download

const file = await ky.get('https://cdn.example.com/archive.zip', {
  onDownloadProgress: (progress, chunk) => {
    console.log(progress.percent, chunk.byteLength);
  },
}).arrayBuffer();

Progress handling reads the body through Ky. The total can be unavailable when the server does not send a usable content length.

Alternatives

PackageRegistryPick it when
axiosnpmYou need older Node or browser coverage, CommonJS compatibility, or axios's established interceptor ecosystem
gotnpmYou are Node-only and want deeper transport controls, streams, pagination, hooks, and retry policy
ofetchnpmYou want a compact universal fetch wrapper with strong Nuxt and server-runtime integration