Skip to content
D
Documentation

Instances and defaults

concept
3 min readUpdated

A Ky instance is a reusable client with custom defaults, so you can configure a particular API once instead of repeating options on every request.

Use ky to compose niche-specific clients: create() starts with new defaults, while extend() inherits and merges its parent's defaults. Both return a new KyInstance; neither changes the parent. Requests use standard Fetch inputs and options, with Ky's additional options.

How defaults compose

An instance stores defaults. Extending it combines those defaults with another option layer; making a request combines the resulting defaults with that request's options.

mermaid
flowchart TD
    A["ky"] -->|"create(defaults)"| B["API client: new defaults"]
    B -->|"extend(options)"| C["Specialized client: merged defaults"]
    C -->|"get(input, options)"| D["Request: defaults merged with request options"]
    B -->|"create(defaults)"| E["Independent client: no inherited defaults"]

The merge is not a blanket replacement:

OptionWhat happens when you add another layer
headersNames merge case-insensitively; a later value replaces the same header.
hooksHook arrays append, preserving parent hooks before added hooks.
searchParamsParameters accumulate, including repeated names.
contextTop-level properties merge; nested objects replace rather than merge.
signalSignals combine when the runtime supports AbortSignal.any().
Scalar options such as timeoutA later value replaces the inherited value.

Plain objects generally deep-merge and arrays append. Use replaceOption when an inherited merged value must be replaced instead.

Create and specialize a client

Pass an Options object to create(). This example uses baseUrl to resolve relative inputs and a beforeRequest hook to show each outgoing URL and header. Run these TypeScript examples in Node.js or a browser with Fetch support; the absolute API addresses also work as URL bases in Node.js. Point them at your service to consume its responses.

ts
import ky, {type Options} from 'ky';

const defaults: Options = {
	baseUrl: 'https://api.example.com/api/',
	timeout: 5000,
	headers: {'x-client': 'dashboard'},
	hooks: {
		beforeRequest: [({request}) => {
			console.log(request.url, request.headers.get('x-client'));
		}],
	},
};

const api = ky.create(defaults);
const usersApi = api.extend(parentOptions => ({
	baseUrl: new URL('users/', parentOptions.baseUrl),
}));

async function main() {
	const user = await usersApi.get('123', {
		headers: {'x-client': 'user-details'},
	}).json();
	console.log(user);

	const version = await api.get('version').text();
	console.log(version);

	const publicApi = api.create({
		baseUrl: 'https://api.example.com/public/',
	});
	const status = await publicApi.get('status').text();
	console.log(status);
}

main().catch(console.error);

The specialized client requests https://api.example.com/api/users/123 with x-client: user-details. The parent still requests https://api.example.com/api/version with x-client: dashboard. The function passed to extend() reads parent defaults and returns the options to merge with them.

The publicApi request uses /public/status without inheriting the parent's x-client header, hook, or 5000 ms timeout. New defaults do not disable Ky's built-in behavior: without a configured timeout, Ky uses 10000 ms.

The .json() call returns the parsed response body, typed as unknown here; .text() returns a string. Look at your service's response rather than expecting a particular body from these example endpoints.

Keep the trailing slash in a baseUrl that contains a path. A page-relative input such as users extends that path; an origin-relative input such as /users resolves from the origin instead. See Resolve request URLs.

Replace or remove inherited values

Wrapping a container with replaceOption() replaces that entire inherited container. This example replaces both the header defaults and the hooks, so the request uses the new logging hook rather than the parent's hook.

ts
import ky, {replaceOption} from 'ky';

const api = ky.create({
	baseUrl: 'https://api.example.com/',
	headers: {'x-client': 'dashboard', 'x-private': 'internal'},
	hooks: {
		beforeRequest: [({request}) => {
			console.log('Parent request:', request.url);
		}],
	},
});

const publicApi = api.extend({
	headers: replaceOption({accept: 'application/json'}),
	hooks: replaceOption({
		beforeRequest: [({request}) => {
			console.log('Public request:', request.url);
			console.log('Inherited private header:', request.headers.has('x-private'));
		}],
	}),
});

async function main() {
	const result = await publicApi.get('public/catalog').json();
	console.log(result);
}

main().catch(console.error);

The request keeps the inherited baseUrl, but neither x-client nor x-private remains in its header defaults. The public hook runs; the parent hook does not. Replacing headers does not prevent a retained hook from adding headers later, so also review inherited authentication hooks when creating a public client.

Choose the narrowest removal you need:

  • headers: {'x-private': undefined} removes one inherited header. In a plain object, the string 'undefined' is a header value, not a deletion marker.
  • hooks: {beforeRequest: undefined} clears only that hook stage.
  • hooks: {beforeRequest: replaceOption([])} replaces only that stage's array; wrapping the whole hooks object replaces all stages.
  • headers: undefined, hooks: undefined, or context: undefined clears the corresponding inherited container.

An empty hook array without replaceOption() appends nothing; it does not clear parent hooks. Likewise, searchParams: {tag: 'new'} can add another tag rather than replace the inherited one. Wrap searchParams to replace inherited option parameters. Parameters already in the input URL still participate in URL merging; an object entry with an undefined value deletes that parameter from the URL.

Pass context to hooks

Use context for request metadata without putting it into the request itself. Hooks receive it through options.context, which is always an object and defaults to {}. This example follows the authentication-token pattern: the hook reads a token and explicitly adds the authorization header.

ts
import ky from 'ky';

const api = ky.create({
	baseUrl: 'https://api.example.com/',
	context: {
		client: 'dashboard',
		metadata: {source: 'navigation'},
	},
	hooks: {
		beforeRequest: [({request, options}) => {
			const {token} = options.context;
			if (typeof token === 'string') {
				request.headers.set('authorization', `Bearer ${token}`);
			}
			console.log(options.context);
		}],
	},
});

const usersApi = api.extend({
	context: {metadata: {section: 'users'}},
});

async function main() {
	const result = await usersApi.get('users', {
		context: {token: 'example-token'},
	}).json();
	console.log(result);
}

main().catch(console.error);

The hook sees client, metadata, and token. Its metadata value is {section: 'users'}: the parent's nested source property is gone because context merges shallowly. Replace the example token with your credential. See Authenticate requests for authentication behavior.

Inherited abort signals

Passing a new signal to extend() normally combines it with inherited signals, rather than replacing them. In a runtime with AbortSignal.any(), aborting either controller cancels requests using the combined signal. Request-level signals use the same option merge.

Use signal: replaceOption(controller.signal) to replace inherited signals, or signal: undefined to remove them. An already-aborted inherited signal otherwise affects later requests too. Without runtime support for AbortSignal.any(), Ky's option merge uses the last signal instead of combining signals.

See Cancel requests for cancellation procedures and Request lifecycle for the stages in which inherited hooks run.

Was this page helpful?