# Instances and defaults

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`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) to compose niche-specific clients: `create()` starts with new defaults, while `extend()` inherits and merges its parent's defaults. Both return a new [`KyInstance`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#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:

| Option | What happens when you add another layer |
| --- | --- |
| `headers` | Names merge case-insensitively; a later value replaces the same header. |
| `hooks` | Hook arrays append, preserving parent hooks before added hooks. |
| `searchParams` | Parameters accumulate, including repeated names. |
| `context` | Top-level properties merge; nested objects replace rather than merge. |
| `signal` | Signals combine when the runtime supports `AbortSignal.any()`. |
| Scalar options such as `timeout` | A later value replaces the inherited value. |

Plain objects generally deep-merge and arrays append. Use [`replaceOption`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#replaceoption) when an inherited merged value must be replaced instead.

## Create and specialize a client

Pass an [`Options`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-options#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](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/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](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/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](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/cancel-requests) for cancellation procedures and [Request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle) for the stages in which inherited hooks run.
