Ky is an HTTP client based on Fetch: pass Fetch-compatible input and options, then use method shortcuts and response-body methods to write common requests directly.
mermaidflowchart LR A["ky instance + request options"] --> B["init hooks"] B --> C["beforeRequest hooks"] C --> D["Fetch attempt"] D --> E["afterResponse hooks"] E --> F{"Response accepted?"} F -->|"yes"| G["Response and body shortcut"] F -->|"no"| H["retry rules / shouldRetry"] H -->|"retry"| I["beforeRetry hooks"] I --> D H -->|"stop"| J["Ky error"]
Start with Fetch, then use Ky's shortcuts
The default ky export accepts the same input and options as Fetch. Its method shortcuts set the HTTP method, and its response promise exposes body methods such as .json() without requiring you to await a Response first. The JSON result defaults to unknown, rather than any; a non-2xx response throws an HTTPError by default after redirects are followed.
tsimport ky from 'ky';
async function main(): Promise<void> {
const user = await ky.get('https://api.example.com/users/42').json();
console.log(user);
}
void main();
This sends a GET request and resolves user with the parsed response body. Check the value's shape before treating the default unknown result as a particular application type.
Give each use case its own defaults
Use ky.create() to start an instance with its own defaults. Use extend() when a use case needs to inherit and adjust a parent instance's defaults. Extension merges options by default: headers merge and hook arrays append. Wrap a merged option with replaceOption when the child must replace it instead.
tsimport ky, {replaceOption} from 'ky';
const api = ky.create({
baseUrl: 'https://api.example.com/v1/',
headers: {'x-service': 'catalog'},
hooks: {
beforeRequest: [({request}) => {
request.headers.set('x-client', 'web');
}],
},
});
const usersApi = api.extend({
hooks: {
beforeRequest: [({request}) => {
request.headers.set('x-feature', 'users');
}],
},
});
const isolatedApi = api.extend({
hooks: replaceOption({
beforeRequest: [({request}) => {
request.headers.set('x-feature', 'isolated-users');
}],
}),
});
async function main(): Promise<void> {
const user = await usersApi.get('users/42').json();
console.log(user);
const isolatedUser = await isolatedApi.get('users/43').json();
console.log(isolatedUser);
}
void main();
Both requests resolve to their parsed response bodies. The first request uses the inherited service header and both beforeRequest hooks; the second uses the service header and only the replacement hook. api keeps its own defaults in either case.
Put request work at the right hook stage
Hooks run at defined points in the request lifecycle. beforeRequest runs before the initial request is sent, while afterResponse receives a clone that you can inspect and may replace by returning a Response. Hook functions run serially. Use the stage that matches the action rather than treating hooks as interchangeable callbacks.
tsimport ky from 'ky';
const measuredApi = ky.extend({
hooks: {
beforeRequest: [({request}) => {
request.headers.set('x-client', 'web');
}],
afterResponse: [({response}) => {
console.info('Received response', response.status);
}],
},
});
async function main(): Promise<void> {
const body = await measuredApi.get('https://api.example.com/catalog').json();
console.log(body);
}
void main();
The request carries the client header; the response hook logs its status, and the call resolves with the parsed body if the response succeeds. Other hook stages include beforeRetry and beforeError; use beforeRetry for work after Ky accepts a retry and beforeError to modify an error before Ky throws it.
Separate retry decisions from retry work
Ky retries eligible failures according to its retry limit, allowed methods and status codes, and delay rules. shouldRetry customizes whether a retry happens; Ky calls it only after the retry limit and method checks pass. beforeRetry runs only after a retry is confirmed, so it is the place to adjust the outgoing retry request or observe the attempt.
tsimport ky, {isHTTPError} from 'ky';
const retryingApi = ky.extend({
retry: {
limit: 2,
methods: ['get'],
shouldRetry: ({error}) => isHTTPError(error) && error.response.status === 503,
},
hooks: {
beforeRetry: [({retryCount}) => {
console.info('Retry attempt', retryCount);
}],
},
});
async function main(): Promise<void> {
const report = await retryingApi.get('https://api.example.com/reports').json();
console.log(report);
}
void main();
For a GET that receives status 503, the decision permits a retry until the configured limit is reached, and the hook observes each permitted attempt. The call resolves with parsed response data if an attempt succeeds; otherwise it rejects with the final error. Ky also supports a forced retry from afterResponse when a response's content calls for another attempt, including a successful status; that retry still respects the configured limit.
Handle lifecycle errors separately from data validation
Use Ky's guards to narrow a caught value by failure type. isHTTPError identifies a response with an error status, isNetworkError identifies a network failure, and isTimeoutError identifies a timeout. isKyError identifies Ky lifecycle errors as a group.
tsimport ky, {isHTTPError, isKyError, isNetworkError, isTimeoutError} from 'ky';
async function main(): Promise<void> {
try {
const result = await ky.get('https://api.example.com/users/42').json();
console.log(result);
} catch (error) {
if (isHTTPError(error)) {
console.error('HTTP status', error.response.status);
} else if (isNetworkError(error)) {
console.error('Network failure for', error.request.url);
} else if (isTimeoutError(error)) {
console.error('Timed out request', error.request.url);
} else if (isKyError(error)) {
console.error('Other Ky lifecycle error', error.message);
} else {
throw error;
}
}
}
void main();
The guards narrow the caught value so each branch can use the fields for that failure. A SchemaValidationError is different: the HTTP request succeeded, but the supplied Standard Schema rejected the returned data, so it is not a Ky lifecycle error and does not match isKyError(). See Ky errors and validation for the consumed response-body detail when handling HTTPError, and validate JSON responses for schema validation.
Was this page helpful?