Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions docs/openapi-ts/clients/ky.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Ky v1 Client
description: Generate a type-safe Ky v1 client from OpenAPI with the Ky client for openapi-ts. Fully compatible with validators, transformers, and all core features.
title: Ky Client
description: Generate a type-safe Ky 2 client from OpenAPI with the Ky client for openapi-ts. Fully compatible with validators, transformers, and all core features.
---

<script setup lang="ts">
Expand All @@ -11,8 +11,8 @@ import { sebastiaanWouters } from '@data/people.js';
</script>

<Heading>
<h1>Ky<span class="sr-only"> v1</span></h1>
<VersionLabel value="v1" />
<h1>Ky<span class="sr-only"> v2</span></h1>
<VersionLabel value="v2" />
</Heading>

### About
Expand All @@ -23,7 +23,7 @@ The Ky client for Hey API generates a type-safe client from your OpenAPI spec, f

### Collaborators

<AuthorsList :people="[sebastiaanWouters]" />
<AuthorsList :people="[sebastiaanWouters,atomicpages]" />

## Features

Expand Down Expand Up @@ -62,6 +62,22 @@ npx @hey-api/openapi-ts \

The Ky client is built as a thin wrapper on top of Ky, extending its functionality to work with Hey API. If you're already familiar with Ky, configuring your client will feel like working directly with Ky.

The generated client targets **Ky 2.x** (Ky 2 requires [Node.js 22](https://github.com/sindresorhus/ky/releases/tag/v2.0.0) or newer). Install `ky@^2` in your application.

### Ky options and upgrading from Ky 1

Hey API types omit several Ky options that the wrapper sets itself (for example `method`, `body`, and `prefix`). For any other Ky settings, pass them in **`kyOptions`** or on the top-level config where the generated types allow it.

If you are upgrading from Ky 1, rename **`prefixUrl` to `prefix`** anywhere you pass Ky configuration (including inside `kyOptions`). Ky 2 also adds a standard **`baseUrl`** option for URL resolution; see the [Ky v2.0.0 release notes](https://github.com/sindresorhus/ky/releases/tag/v2.0.0) for hooks, `HTTPError.data`, and other breaking changes.

```js
client.setConfig({
kyOptions: {
prefix: 'https://api.example.com/v1/', // was `prefixUrl` in Ky 1
},
});
```

When we installed the client above, it created a [`client.gen.ts`](/openapi-ts/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that.

### `setConfig()`
Expand Down
2 changes: 1 addition & 1 deletion examples/openapi-ts-ky/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@radix-ui/react-form": "0.1.1",
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/themes": "3.1.6",
"ky": "1.14.0",
"ky": "2.0.0",
"react": "19.0.0",
"react-dom": "19.0.0"
},
Expand Down
4 changes: 1 addition & 3 deletions examples/openapi-ts-ky/src/client/client.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,4 @@ export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;

export const client = createClient(
createConfig<ClientOptions2>({ baseUrl: 'https://petstore3.swagger.io/api/v3' }),
);
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: '/api/v3' }));
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

Verified the base url remains correct

61 changes: 48 additions & 13 deletions examples/openapi-ts-ky/src/client/client/client.gen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts

import type { HTTPError, Options as KyOptions } from 'ky';
import ky from 'ky';
import type { Options as KyOptions } from 'ky';
import ky, { isHTTPError } from 'ky';
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was available in later version of ky v1, not required to use, but helps with tsc


import { createSseClient } from '../core/serverSentEvents.gen';
import type { HttpMethod } from '../core/types.gen';
Expand Down Expand Up @@ -83,22 +83,44 @@ export const createClient = (config: Config = {}): Client => {
request: Request,
opts: ResolvedRequestOptions,
interceptorsMiddleware: Middleware<Request, Response, unknown, ResolvedRequestOptions>,
kyHttpError?: { bodyConsumed: true; data: unknown },
) => {
const result = {
request,
response,
};

const textError = await response.text();
let jsonError: unknown;
let error: unknown;

try {
jsonError = JSON.parse(textError);
} catch {
jsonError = undefined;
if (kyHttpError) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is due to:

  1. Make .json() throw on empty bodies and 204 responses instead of returning an empty string (Make json() throw on empty responses sindresorhus/ky#854) sindresorhus/ky@1b8e1ff
  2. Make beforeError hook receive all errors, not just HTTPError (Make beforeError hook receive all errors sindresorhus/ky#829) sindresorhus/ky@101c74b

if (kyHttpError.data !== undefined) {
if (typeof kyHttpError.data === 'string') {
let jsonError: unknown;
try {
jsonError = JSON.parse(kyHttpError.data);
} catch {
jsonError = undefined;
}
error = jsonError ?? kyHttpError.data;
} else {
error = kyHttpError.data;
}
} else {
error = '';
}
} else {
const textError = await response.text();
let jsonError: unknown;

try {
jsonError = JSON.parse(textError);
} catch {
jsonError = undefined;
}

error = jsonError ?? textError;
}

const error = jsonError ?? textError;
let finalError = error;

for (const fn of interceptorsMiddleware.error.fns) {
Expand Down Expand Up @@ -174,17 +196,30 @@ export const createClient = (config: Config = {}): Client => {
try {
response = await kyInstance(request, kyOptions);
} catch (error) {
if (error && typeof error === 'object' && 'response' in error) {
const httpError = error as HTTPError;
response = httpError.response;
if (isHTTPError(error)) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response = error.response;

for (const fn of interceptors.response.fns) {
if (fn) {
response = await fn(response, request, opts);
}
}

return parseErrorResponse(response, request, opts, interceptors);
if (error.data !== undefined) {
return parseErrorResponse(response, request, opts, interceptors, {
bodyConsumed: true,
data: error.data,
});
}

if (!response.bodyUsed) {
return parseErrorResponse(response, request, opts, interceptors);
}

return parseErrorResponse(response, request, opts, interceptors, {
bodyConsumed: true,
data: undefined,
});
}

throw error;
Expand Down
4 changes: 2 additions & 2 deletions examples/openapi-ts-ky/src/client/client/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface RetryOptions {

export interface Config<T extends ClientOptions = ClientOptions>
extends
Omit<KyOptions, 'body' | 'headers' | 'method' | 'prefixUrl' | 'retry' | 'throwHttpErrors'>,
Omit<KyOptions, 'body' | 'headers' | 'method' | 'prefix' | 'retry' | 'throwHttpErrors'>,
CoreConfig {
/**
* Base URL for all requests made by this client.
Expand All @@ -48,7 +48,7 @@ export interface Config<T extends ClientOptions = ClientOptions>
* Additional ky-specific options that will be passed directly to ky.
* This allows you to use any ky option not explicitly exposed in the config.
*/
kyOptions?: Omit<KyOptions, 'method' | 'prefixUrl'>;
kyOptions?: Omit<KyOptions, 'method' | 'prefix'>;
/**
* Return the response data parsed in a specified format. By default, `auto`
* will infer the appropriate method from the `Content-Type` response header.
Expand Down
5 changes: 0 additions & 5 deletions examples/openapi-ts-ky/src/client/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export const OrderSchema = {
},
},
type: 'object',
'x-swagger-router-model': 'io.swagger.petstore.model.Order',
xml: {
name: 'order',
},
Expand All @@ -51,7 +50,6 @@ export const CategorySchema = {
},
},
type: 'object',
'x-swagger-router-model': 'io.swagger.petstore.model.Category',
xml: {
name: 'category',
},
Expand Down Expand Up @@ -96,7 +94,6 @@ export const UserSchema = {
},
},
type: 'object',
'x-swagger-router-model': 'io.swagger.petstore.model.User',
xml: {
name: 'user',
},
Expand All @@ -113,7 +110,6 @@ export const TagSchema = {
},
},
type: 'object',
'x-swagger-router-model': 'io.swagger.petstore.model.Tag',
xml: {
name: 'tag',
},
Expand Down Expand Up @@ -162,7 +158,6 @@ export const PetSchema = {
},
required: ['name', 'photoUrls'],
type: 'object',
'x-swagger-router-model': 'io.swagger.petstore.model.Pet',
xml: {
name: 'pet',
},
Expand Down
2 changes: 1 addition & 1 deletion examples/openapi-ts-ky/src/client/types.gen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts

export type ClientOptions = {
baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {});
baseUrl: `${string}://${string}/api/v3` | (string & {});
};

export type Order = {
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-ts-tests/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"cross-spawn": "7.0.6",
"eslint": "9.39.2",
"fastify": "5.7.4",
"ky": "1.14.3",
"ky": "2.0.0",
"node-fetch": "3.3.2",
"nuxt": "3.14.1592",
"ofetch": "1.5.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts

import type { HTTPError, Options as KyOptions } from 'ky';
import ky from 'ky';
import type { Options as KyOptions } from 'ky';
import ky, { isHTTPError } from 'ky';

import { createSseClient } from '../core/serverSentEvents.gen';
import type { HttpMethod } from '../core/types.gen';
Expand Down Expand Up @@ -77,22 +77,44 @@ export const createClient = (config: Config = {}): Client => {
request: Request,
opts: ResolvedRequestOptions,
interceptorsMiddleware: Middleware<Request, Response, unknown, ResolvedRequestOptions>,
kyHttpError?: { bodyConsumed: true; data: unknown },
) => {
const result = {
request,
response,
};

const textError = await response.text();
let jsonError: unknown;
let error: unknown;

try {
jsonError = JSON.parse(textError);
} catch {
jsonError = undefined;
if (kyHttpError) {
if (kyHttpError.data !== undefined) {
if (typeof kyHttpError.data === 'string') {
let jsonError: unknown;
try {
jsonError = JSON.parse(kyHttpError.data);
} catch {
jsonError = undefined;
}
error = jsonError ?? kyHttpError.data;
} else {
error = kyHttpError.data;
}
} else {
error = '';
}
} else {
const textError = await response.text();
let jsonError: unknown;

try {
jsonError = JSON.parse(textError);
} catch {
jsonError = undefined;
}

error = jsonError ?? textError;
}

const error = jsonError ?? textError;
let finalError = error;

for (const fn of interceptorsMiddleware.error.fns) {
Expand Down Expand Up @@ -168,17 +190,30 @@ export const createClient = (config: Config = {}): Client => {
try {
response = await kyInstance(request, kyOptions);
} catch (error) {
if (error && typeof error === 'object' && 'response' in error) {
const httpError = error as HTTPError;
response = httpError.response;
if (isHTTPError(error)) {
response = error.response;

for (const fn of interceptors.response.fns) {
if (fn) {
response = await fn(response, request, opts);
}
}

return parseErrorResponse(response, request, opts, interceptors);
if (error.data !== undefined) {
return parseErrorResponse(response, request, opts, interceptors, {
bodyConsumed: true,
data: error.data,
});
}

if (!response.bodyUsed) {
return parseErrorResponse(response, request, opts, interceptors);
}

return parseErrorResponse(response, request, opts, interceptors, {
bodyConsumed: true,
data: undefined,
});
}

throw error;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface RetryOptions {

export interface Config<T extends ClientOptions = ClientOptions>
extends
Omit<KyOptions, 'body' | 'headers' | 'method' | 'prefixUrl' | 'retry' | 'throwHttpErrors'>,
Omit<KyOptions, 'body' | 'headers' | 'method' | 'prefix' | 'retry' | 'throwHttpErrors'>,
CoreConfig {
/**
* Base URL for all requests made by this client.
Expand All @@ -51,7 +51,7 @@ export interface Config<T extends ClientOptions = ClientOptions>
* Additional ky-specific options that will be passed directly to ky.
* This allows you to use any ky option not explicitly exposed in the config.
*/
kyOptions?: Omit<KyOptions, 'method' | 'prefixUrl'>;
kyOptions?: Omit<KyOptions, 'method' | 'prefix'>;
/**
* Return the response data parsed in a specified format. By default, `auto`
* will infer the appropriate method from the `Content-Type` response header.
Expand Down
Loading
Loading