ky review
Ky 2.0.2 wraps the Fetch API with JSON request shortcuts, automatic errors for non-2xx responses, retries, per-attempt and total timeouts, lifecycle hooks, configured clients, progress callbacks, and Standard Schema response validation. It still returns web `Response` objects, so existing knowledge of fetch, headers, abort signals, and body consumption applies. The 2.0.2 patch fixes deletions and mutation leakage when `searchParams` are changed by an init hook. In our browser bundle test it added 20.5 KB minified and 7.3 KB gzipped, with no package dependencies.
Ky earns its place when a Node 22 or modern-browser project needs one fetch-shaped policy for retries, hooks, JSON, and deadlines. Stay with native fetch for simple calls, and disable Ky retries for large streaming bodies.
We installed it
| Install | ✓ · 0.3s | 1 package on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 7.3 KB | gzipped (20.5 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does ky install cleanly?
Yes. In a fresh container with an empty cache, npm install ky finished in 0.3s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does ky add to a browser bundle?
7.3 KB gzipped (20.5 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does ky work with both ESM and CommonJS?
Yes. Both import 'ky' and require('ky') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does ky include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
ky or axios: which should you use?
axios: Use it for projects needing its interceptor model, wider historical runtime coverage, adapters, and a larger integration catalog. Ky earns its place when a Node 22 or modern-browser project needs one fetch-shaped policy for retries, hooks, JSON, and deadlines.
When should you not use ky?
The application makes a few direct requests with no shared policy. Native fetch already ships in every runtime targeted by Ky 2 and avoids another behavior layer.
Use it if
- Your browser or Node 22 application already uses fetch concepts but repeats status checks, JSON headers, retries, and timeout wiring.
- Several API clients need separate base URLs, headers, hook chains, and retry policies while retaining standard Request and Response objects.
- TypeScript should treat unannotated JSON as `unknown`, with optional runtime validation through a Standard Schema implementation.
- A request layer needs bounded retries plus separate per-attempt and whole-operation deadlines.
- The application makes a few direct requests with no shared policy. Native fetch already ships in every runtime targeted by Ky 2 and avoids another behavior layer.
- Production still uses Node 20 or earlier. Ky 2.0.2 declares Node 22 as the minimum engine even though modern browsers, Bun, and Deno are also targets.
- Large streaming uploads must stay memory-bounded while retrying. The versioned README says retry support uses `tee()` and buffers the entire `ReadableStream`; disable retries or choose transport logic built for replayable streams.
- Callers expect `response.json()` to tolerate status 204 or an empty body. Ky 2 throws a parse error in both cases, so code must inspect the response before choosing the JSON shortcut.
- Existing error handlers read `error.response.json()`. Ky 2 consumes and parses HTTP error bodies before throwing, placing the result on `error.data`; the response body methods cannot be read again.
Setup reality
We installed Ky 2.0.2 in a clean Node 22 Bookworm container. npm completed in 0.3 seconds and left one package using 1 MB. Ky has zero direct dependencies and zero peer dependencies; npm reports 668 KB unpacked. The package includes TypeScript declarations and uses ESM with an exports map. Both require() and ESM import worked under Node 22.23.2. npm audit found zero known vulnerabilities at every severity.
Our esbuild browser check produced 20.5 KB minified and 7.3 KB gzipped. No credentials or config file are required, though browser CORS policy still applies. Server-side relative URLs need a usable baseUrl; a base containing a path should end with / or standard URL resolution can replace its final segment. prefix performs string joining and treats a leading slash differently, so choose it only when that behavior is wanted.
Ky retries selected idempotent methods and status codes with a default limit of two. The normal timeout applies to each attempt, while totalTimeout bounds attempts and delays together. Timeout failures are excluded from retries unless retryOnTimeout is enabled. Retried streams are split with tee() and buffered, which can turn a large upload into a memory problem. Set retry: {limit: 0} for a body that should not be replayed.
Version 2 gives hooks one state object such as {request, options, retryCount}. An init hook is synchronous; other hook arrays run serially. HTTPError.data contains the already parsed error body, while network and timeout errors have no response. A generic passed to .json<T>() changes TypeScript's belief only. Pass a Standard Schema validator when untrusted JSON needs a runtime check. The 2.0.2 patch specifically prevents init-hook search parameter edits from leaking across requests.
Patterns
Request a typed JSON value read-typed-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 type argument performs no runtime validation. Without an argument or schema, Ky types the JSON result as `unknown`.
Create a record with JSON send-json-body
const created = await ky.post('https://api.example.com/users', {
json: {name: 'Mina'},
}).json<{id: string}>();The `json` option stringifies the value and sets the content type. The response shortcut throws on an empty body or status 204.
Set shared URL and deadlines create-api-client
const api = ky.create({
baseUrl: 'https://api.example.com/v1/',
timeout: 5_000,
totalTimeout: 20_000,
headers: {'X-Client': 'dashboard'},
});
const users = await api.get('users').json();Keep the trailing slash on a base URL path. Without it, resolving `users` replaces the last path segment.
Add query values to an existing URL merge-search-parameters
const page = await ky.get('https://api.example.com/search?lang=en', {
searchParams: {q: 'http clients', page: 2},
}).json();Ky 2 merges these values with the input query. Version 2.0.2 fixes mutation leakage when init hooks edit tuple-form parameters.
Add a bearer token in a hook attach-authentication
const authed = api.extend({
hooks: {
beforeRequest: [({request}) => {
const token = readToken();
if (token) request.headers.set('Authorization', `Bearer ${token}`);
}],
},
});Ky 2 hooks receive one state object. `beforeRequest` runs before retry handling; use `beforeRetry` when credentials must change for another attempt.
Add jitter and an overall deadline bound-retry-window
const report = await api.get('reports/latest', {
retry: {
limit: 3,
jitter: true,
retryOnTimeout: true,
},
timeout: 4_000,
totalTimeout: 15_000,
}).json();`timeout` applies per attempt; `totalTimeout` includes attempts and wait periods. Keep automatic retries to requests that are safe to repeat.
Send a stream without replay buffering disable-upload-retries
await ky.post('https://api.example.com/import', {
body: readableStream,
retry: {limit: 0},
headers: {'content-type': 'application/octet-stream'},
});Enabled retries tee and buffer a stream body. A zero limit avoids that memory cost and prevents a second upload attempt.
Read parsed error data inspect-http-failure
import {isHTTPError} from 'ky';
try {
await api.get('users/missing').json();
} catch (error) {
if (isHTTPError(error)) {
console.error(error.response.status, error.data);
return;
}
throw error;
}Ky 2 already consumes the HTTP error body. Read `error.data`; network and timeout errors do not carry a response.
Return non-2xx responses keep-fetch-status-handling
const response = await ky.get('https://api.example.com/health', {
throwHttpErrors: false,
});
if (!response.ok) console.error(response.status);With this option, HTTP failures are returned and are not retried. Network failures and timeouts may still throw.
Abort work from the caller cancel-inflight-request
const controller = new AbortController();
const promise = api.get('reports/slow', {signal: controller.signal}).json();
controller.abort();
await promise;The promise rejects after cancellation. Handle that rejection when aborting is part of normal UI behavior.
Check JSON against a schema validate-json-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 Standard Schema mismatch throws `SchemaValidationError`; compatible Zod versions implement that contract.
Track a response body observe-download-progress
const archive = await ky.get('https://cdn.example.com/archive.zip', {
onDownloadProgress(progress, chunk) {
console.log(progress.transferredBytes, progress.totalBytes, chunk.byteLength);
},
}).arrayBuffer();`totalBytes` may be zero when the server provides no usable length. Ky reads the response body to deliver these callbacks.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| axios | npm | Use it for projects needing its interceptor model, wider historical runtime coverage, adapters, and a larger integration catalog. |
| got | npm | Use it in Node-only services that need deeper stream, pagination, DNS, agent, and transport controls. |
| ofetch | npm | Use it for a small fetch wrapper closely tied to Nuxt, Nitro, and universal server runtime conventions. |
More web frontend guides
postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

