Ky is a Fetch-based HTTP client that builds a request from a Fetch-compatible input and options, then exposes response body shortcuts, typed JSON, and schema validation on the returned promise.
The request and response flow
Pass a string, URL, or Request as the input. Ky applies its options, constructs the Request, performs the fetch, and gives you a response promise whose body methods can be called before awaiting the response object.
mermaidflowchart LR A["Input + options"] --> B["Ky builds Request"] B --> C["Fetch"] C --> D["ResponsePromise"] D --> E["json(), text(), blob(), formData(), arrayBuffer(), bytes()"] E --> F["typed value or schema-validated value"] C --> G["HTTPError for non-2xx"] E --> H["SchemaValidationError for rejected JSON"]
The input accepts the Input type: a string, URL, or Request. The default ky export also provides method shortcuts such as ky.post(). A shortcut sets the request method; the other options remain Fetch options or Ky options.
When you pass json, Ky stringifies the value, uses it as the request body, and sets Content-Type: application/json unless your headers option supplies a content type. Use json instead of manually creating a body for JSON data.
tsimport ky from 'ky';
async function main(): Promise<void> {
const result = await ky.post('https://api.example.com/users', {
json: {name: 'Ada'},
}).json<{id: string; name: string}>();
console.log(result.id, result.name);
}
main();
This sends a JSON POST and returns the parsed response body as the type supplied to json(). The promise returned by the request is a ResponsePromise, so you do not need to await a separate response before selecting a body method.
Choose a response body
The response promise provides json(), text(), formData(), arrayBuffer(), blob(), and bytes() shortcuts. bytes() is available when the runtime supports Response.prototype.bytes().
tsimport ky from 'ky';
async function main(): Promise<void> {
const text = await ky.get('https://api.example.com/status').text();
const file = await ky.get('https://api.example.com/report.pdf').blob();
const raw = await ky.get('https://api.example.com/archive.bin').arrayBuffer();
console.log(text, file.size, raw.byteLength);
}
main();
Each call consumes the response body in the selected representation. If you need response metadata as well, await the promise itself. The resulting KyResponse has the standard Response properties and methods plus Ky's typed json() method.
tsimport ky from 'ky';
async function main(): Promise<void> {
const response = await ky.get('https://api.example.com/status');
console.log(response.status, response.headers.get('content-type'));
const body = await response.json<{status: string}>();
console.log(body.status);
}
main();
Calling json() for an empty response throws because there is no JSON body to parse. When an empty body is part of your protocol, configure parseJson to provide the required handling rather than assuming that json() can parse it.
Type JSON at the boundary
Without a type argument, json() returns unknown. Give the request or the body method a type argument when your application has a known response shape.
tsimport ky from 'ky';
type User = {
id: string;
name: string;
};
async function main(): Promise<void> {
const fromRequestType = await ky<User>('https://api.example.com/users/1').json();
const fromBodyType = await ky('https://api.example.com/users/1').json<User>();
console.log(fromRequestType.name, fromBodyType.id);
}
main();
These type arguments describe the value TypeScript expects; they do not validate the bytes returned by the server. Use a Standard Schema-compatible validator when the response must be checked at runtime.
Validate JSON at runtime
Pass a Standard Schema validator to json(schema). Ky returns the validator's inferred output when validation succeeds and throws SchemaValidationError when it fails.
tsimport ky, {SchemaValidationError} from 'ky';
import {z} from 'zod';
const userSchema = z.object({
name: z.string(),
});
async function main(): Promise<void> {
try {
const user = await ky('https://api.example.com/users/1').json(userSchema);
console.log(user);
} catch (error) {
if (error instanceof SchemaValidationError) {
console.error(error.issues);
} else {
throw error;
}
}
}
main();
Schema validation happens after the request succeeds. SchemaValidationError is therefore not a Ky lifecycle error and is not matched by isKyError().
Separate HTTP failures from missing responses
Ky throws HTTPError for a non-2xx response when throwHttpErrors is enabled. The error retains the KyResponse in error.response, the KyRequest in error.request, normalized options, and a pre-parsed error.data value when Ky can read the error body. Because Ky consumes the body while populating error.data, use error.data rather than calling error.response.json() in this case.
For the error classes and type guards, see How Ky works.
tsimport ky, {isHTTPError} from 'ky';
async function main(): Promise<void> {
try {
await ky.get('https://api.example.com/users/1').json<{name: string}>();
} catch (error) {
if (isHTTPError(error)) {
console.error('Status:', error.response.status);
console.error('Error body:', error.data);
} else {
throw error;
}
}
}
main();
The HTTP branch can inspect the status and parsed error data. The network branch does not try to read a response because none was received. Throw an ordinary error when a caller needs a propagated failure from a retry hook; returning ky.stop produces an undefined response that cannot be followed by .json() or .text().
Was this page helpful?