Ky: a small fetch wrapper for JavaScript HTTP requests
🌳 Tiny & elegant JavaScript HTTP client based on the Fetch API
At a glance
- What is it?
- Ky is an HTTP client built on the Fetch API that adds method shortcuts, automatic error throwing, retries and JSON handling. It is aimed at modern browsers, Node.js, Bun and Deno, and it ships with no dependencies.
- Who is it for?
- Adopt Ky if your runtime is a modern browser, Node.js 22 or newer, Bun or Deno, and you want fetch semantics with retries and typed JSON without pulling in a dependency tree. Skip it if you need Node.js 18 or 20, an axios-style interceptor model, or a client with no reliance on the global Fetch API.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 13 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The gap Ky fills between raw fetch and a full client
Plain fetch returns a Response object and leaves the rest to you. Non-2xx statuses do not throw. There is no retry, no timeout option, no base URL, and no JSON shortcut, so a POST with a JSON body means calling JSON.stringify and setting the content-type header yourself. The README shows exactly that comparison: a fetch version needs a hand-written HTTPError class, a manual response.ok check, and an explicit header object before it can parse the result. Ky packages those steps into one call.
The audience is narrow and specific. Ky targets modern browsers, Node.js, Bun and Deno, and it is a single package with no dependencies, which matters when bundle size is a constraint. If you are writing a small script against one endpoint, raw fetch is fine and Ky adds a layer you do not need. Ky pays off when a project makes many requests across several files and wants consistent defaults for retries, timeouts, prefixes and error handling.
What happens between ky.get() and the parsed body
Ky is a thin layer over fetch, not a replacement transport. The input and options are the same as fetch, plus additional options, and the return value is a Response object with body methods added for convenience. That design decision is visible in the API: you can call ky.get(input).json() directly, and the Accept header is set according to the body method you use.
The error behaviour is the sharpest difference from fetch. When you use one of the body shortcuts, a status outside 200 to 299 throws an HTTPError, evaluated after redirects. That means a 404 from an API surfaces as an exception rather than a falsy response.ok check, which is convenient in async code and surprising if you expected fetch semantics. The README also notes that .json() throws on an empty body because it cannot be parsed, and points to the parseJson option for custom handling of that case. That is a real trap: a 204 No Content response will throw if you call .json() on it.
On top of that sit the options that justify the wrapper: a json option that stringifies a value and sets the content-type unless you override it in headers, a searchParams option that merges with parameters already in the URL, a baseUrl option, a prefix option, timeout support, upload and download progress, and hooks. Instances let you bake custom defaults into a reusable client. Retries are built in for failed requests. None of this changes what fetch does on the wire; it changes how much code you write around it.
Installing Ky and making a first typed request
Ky installs from npm as a single package. The README gives one command:
npm install kyAfter that, the package resolves through its exports map to distribution/index.js with types at distribution/index.d.ts, and the package declares "type": "module", so it is ESM only. A CommonJS require will not work without a loader or bundler that understands ESM.
The smallest useful call posts JSON and parses the reply. Note that the json option replaces body and sets the content-type header for you:
import ky from 'ky';
const json = await ky.post('https://example.com', {json: {foo: true}}).json();
console.log(json);The README shows the response as an object with a data field. The important part is not the payload but the shape of the call: no manual stringify, no header object, and a thrown error if the status is not 2xx.
TypeScript users get a type parameter on .json(). It defaults to unknown rather than any, so you opt in to a type instead of inheriting an unsafe one. The README shows three forms, and all three are valid:
import ky from 'ky';
// user1 is unknown
const user1 = await ky('/api/users/1').json();
// user2 is a User
const user2 = await ky<User>('/api/users/2').json();
// user3 is a User
const user3 = await ky('/api/users/3').json<User>();For runtime checking rather than compile-time types, .json() accepts a Standard Schema compatible validator such as Zod 3.24 or later and throws a SchemaValidationError on failure, with the issues available on the error. Deno users import from a CDN instead of npm, for example https://esm.sh/ky; jsdelivr and unpkg are listed as alternatives.
Where Ky is the wrong tool
The package.json engines field requires Node.js 22 or newer. That is the first filter. If you maintain a service on Node.js 18 or 20, the published package declares a runtime you do not have, and the README does not describe a fallback build for older Node versions. Treat that as a hard constraint rather than a suggestion to ignore.
The second constraint is the Fetch API itself. Ky is based on fetch, so it inherits fetch's model: no request cancellation outside AbortController, no built-in progress events on the Response object in the way XHR provided them, and behaviour that depends on the runtime's fetch implementation. The README lists upload and download progress as a benefit, but it does not document how that interacts with every runtime, and the README is silent on rollback or migration guidance between major versions.
The third is error semantics. Throwing on non-2xx is convenient until you need to handle a 404 as a normal branch. You can catch HTTPError, but every call site now carries a try block. The .json() behaviour on empty bodies is a related edge: a 204 response and a call to .json() do not mix without the parseJson option. If your API leans on empty responses, budget time for that.
Finally, Ky is not a drop-in axios replacement. It has no interceptor API in the axios sense. It has hooks, which is a different mechanism, and code written around axios interceptors will need rewriting rather than a changed import.
Ky vs Axios and Ky vs fetch: what actually differs
The most common question is Ky versus Axios, and the difference is architectural. Axios ships its own HTTP adapter and works on top of XMLHttpRequest in the browser and the http module in Node. Ky delegates to the global fetch and adds convenience around it. That means Ky has no dependencies and stays small, while Axios carries its own request machinery and its own interceptor model. If your codebase is built on axios interceptors for auth tokens and logging, Ky's hooks are a different abstraction and the migration is not mechanical.
Ky versus fetch is a smaller gap. Fetch is the platform primitive; Ky is a wrapper that adds method shortcuts, retry, timeout, a base URL, instances with defaults, and the JSON option. The README's own example shows the fetch version needing an HTTPError class and a manual ok check to reach the same result. If you already have a small helper module that does that, you are halfway to Ky and should compare the two before adding a dependency.
One detail worth noting for anyone comparing clients: Ky does not use the body option for JSON. It uses json, and any Content-Type you set in headers takes precedence over the one Ky would set. A Content-Type on a Request input is replaced, because the Request constructor sets one automatically from its body. These are small rules, but they decide whether your first request works.
Maintenance, licence and upgrade cost
Ky is MIT licensed, and the repository is not archived. The last push was on 2026-09-16, and the most recent release listed is v2.1.0 on 2026-08-28, with v2.0.2 and v2.0.1 earlier in 2026. That release cadence suggests the project is still moving, but the README itself carries a warning worth reading carefully: the readme is for the next version of Ky, and it links to the npm page for the current version. So the documentation you read on the repository may describe behaviour that is not yet in the version npm installs by default. Check which version you actually get before trusting an option you read about.
Upgrade cost is tied to the major version boundary. The 2.x line requires Node.js 22 or newer, so a move from 1.x is also a runtime decision, not just a dependency bump. The README does not document a codemod or an upgrade guide for that transition, so plan to read the release notes and test your call sites, particularly any place that relies on .json() against empty bodies or on the exact error type thrown.
The MIT licence is permissive and places few obligations on how you redistribute the package. That is a statement about the licence text, not legal advice; if your organisation has a policy on dependency licences, run it through that process.
Editorial conclusion
Adopt Ky if your runtime is a modern browser, Node.js 22 or newer, Bun or Deno, and you want fetch semantics with retries and typed JSON without pulling in a dependency tree. Skip it if you need Node.js 18 or 20, an axios-style interceptor model, or a client with no reliance on the global Fetch API. Before committing, check the engines field in package.json against your deployment target, confirm which version npm install ky resolves for you, and read the parseJson option if your endpoints can return empty bodies.
Frequently asked questions
Which is better, axios or ky?
It depends on what you need from the client. Axios ships its own HTTP adapter and an interceptor model, while Ky is built on the Fetch API, has no dependencies, and offers hooks instead of interceptors. Ky also requires Node.js 22 or newer according to its package.json engines field.
How do I install ky?
Run npm install ky. The package is ESM only, with types at distribution/index.d.ts, and Deno users can import it from a CDN such as https://esm.sh/ky instead.
Does ky throw an error on a 404 response?
Yes, when you use a body shortcut such as .json(). The README states that these throw an HTTPError if the response status is not in the range of 200 to 299, evaluated after redirects.
Why does ky's .json() throw on an empty response body?
The README notes that .json() will throw if the body is empty, because it cannot be parsed. It points to the parseJson option for adding custom handling of empty bodies.
Which runtimes does ky support?
Ky targets modern browsers, Node.js, Bun and Deno. The package.json engines field sets the Node requirement at 22 or newer.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sindresorhus-ky)
Community notes