Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/chilled-tools-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/openapi-ts": patch
---

**clients**: add support for Ky client
24 changes: 24 additions & 0 deletions examples/openapi-ts-ky/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
7 changes: 7 additions & 0 deletions examples/openapi-ts-ky/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# @example/openapi-ts-ky

## 0.0.1

### Patch Changes

- Initial release of ky client example
13 changes: 13 additions & 0 deletions examples/openapi-ts-ky/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hey API + Fetch API Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
20 changes: 20 additions & 0 deletions examples/openapi-ts-ky/openapi-ts.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig({
input:
'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml',
output: {
format: 'prettier',
lint: 'eslint',
path: './src/client',
},
plugins: [
'@hey-api/client-ky',
'@hey-api/schemas',
'@hey-api/sdk',
{
enums: 'javascript',
name: '@hey-api/typescript',
},
],
});
40 changes: 40 additions & 0 deletions examples/openapi-ts-ky/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@example/openapi-ts-ky",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"build": "tsc && vite build",
"dev": "vite",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"openapi-ts": "openapi-ts",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-form": "0.1.1",
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/themes": "3.1.6",
"ky": "1.14.0",
"react": "19.0.0",
"react-dom": "19.0.0"
},
"devDependencies": {
"@config/vite-base": "workspace:*",
"@hey-api/openapi-ts": "workspace:*",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1",
"@typescript-eslint/eslint-plugin": "8.29.1",
"@typescript-eslint/parser": "8.29.1",
"@vitejs/plugin-react": "4.4.0-beta.1",
"autoprefixer": "10.4.19",
"eslint": "9.17.0",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.7",
"postcss": "8.4.41",
"prettier": "3.4.2",
"tailwindcss": "3.4.9",
"typescript": "5.8.3",
"vite": "7.1.2"
}
}
6 changes: 6 additions & 0 deletions examples/openapi-ts-ky/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
autoprefixer: {},
tailwindcss: {},
},
};
3 changes: 3 additions & 0 deletions examples/openapi-ts-ky/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
243 changes: 243 additions & 0 deletions examples/openapi-ts-ky/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import './App.css';

import * as Form from '@radix-ui/react-form';
import { DownloadIcon, PlusIcon, ReloadIcon } from '@radix-ui/react-icons';
import {
Avatar,
Box,
Button,
Card,
Container,
Flex,
Heading,
Section,
Text,
TextField,
} from '@radix-ui/themes';
import { useState } from 'react';

import { createClient } from './client/client';
import { PetSchema } from './client/schemas.gen';
import { addPet, getPetById, updatePet } from './client/sdk.gen';
import type { Pet } from './client/types.gen';

const localClient = createClient({
// set default base url for requests made by this client
baseUrl: 'https://petstore3.swagger.io/api/v3',
/**
* Set default headers only for requests made by this client. This is to
* demonstrate local clients and their configuration taking precedence over
* internal service client.
*/
headers: {
Authorization: 'Bearer <token_from_local_client>',
},
});

localClient.interceptors.request.use((request, options) => {
// Middleware is great for adding authorization tokens to requests made to
// protected paths. Headers are set randomly here to allow surfacing the
// default headers, too.
if (
options.url === '/pet/{petId}' &&
options.method === 'GET' &&
Math.random() < 0.5
) {
request.headers.set('Authorization', 'Bearer <token_from_interceptor>');
}
return request;
});

localClient.interceptors.error.use((error) => {
console.log(error);
return error;
});

function App() {
const [pet, setPet] = useState<Pet>();
const [isRequiredNameError, setIsRequiredNameError] = useState(false);

const onAddPet = async (formData: FormData) => {
// simple form field validation to demonstrate using schemas
if (PetSchema.required.includes('name') && !formData.get('name')) {
setIsRequiredNameError(true);
return;
}

const { data, error } = await addPet({
body: {
category: {
id: 0,
name: formData.get('category') as string,
},
id: 0,
name: formData.get('name') as string,
photoUrls: ['string'],
status: 'available',
tags: [
{
id: 0,
name: 'string',
},
],
},
});
if (error) {
console.log(error);
return;
}
setPet(data!);
setIsRequiredNameError(false);
};

const onGetPetById = async () => {
const { data, error } = await getPetById({
client: localClient,
path: {
// random id 1-10
petId: Math.floor(Math.random() * (10 - 1 + 1) + 1),
},
});
if (error) {
console.log(error);
return;
}
setPet(data!);
};

const onUpdatePet = async () => {
const { data, error } = await updatePet({
body: {
category: {
id: 0,
name: 'Cats',
},
id: 2,
name: 'Updated Kitty',
photoUrls: ['string'],
status: 'available',
tags: [
{
id: 0,
name: 'string',
},
],
},
// setting headers per request
headers: {
Authorization: 'Bearer <token_from_method>',
},
});
if (error) {
console.log(error);
return;
}
setPet(data!);
};

return (
<Box
style={{ background: 'var(--gray-a2)', borderRadius: 'var(--radius-3)' }}
>
<Container size="1">
<Section size="1" />
<Flex align="center">
<a className="shrink-0" href="https://heyapi.dev/" target="_blank">
<img
src="https://heyapi.dev/logo.png"
className="h-16 w-16 transition duration-300 will-change-auto"
alt="Hey API logo"
/>
</a>
<Heading>@hey-api/openapi-ts 🤝 Fetch API</Heading>
</Flex>
<Section size="1" />
<Flex direction="column" gapY="2">
<Box maxWidth="240px">
<Card>
<Flex gap="3" align="center">
<Avatar
size="3"
src={pet?.photoUrls[0]}
radius="full"
fallback={pet?.name.slice(0, 1) ?? 'N'}
/>
<Box>
<Text as="div" size="2" weight="bold">
Name: {pet?.name ?? 'N/A'}
</Text>
<Text as="div" size="2" color="gray">
Category: {pet?.category?.name ?? 'N/A'}
</Text>
</Box>
</Flex>
</Card>
</Box>
<Button onClick={onGetPetById}>
<DownloadIcon /> Get Random Pet
</Button>
</Flex>
<Section size="1" />
<Flex direction="column" gapY="2">
<Form.Root
className="w-[260px]"
onSubmit={(event) => {
event.preventDefault();
onAddPet(new FormData(event.currentTarget));
}}
>
<Form.Field className="grid mb-[10px]" name="email">
<div className="flex items-baseline justify-between">
<Form.Label className="text-[15px] font-medium leading-[35px] text-white">
Name
</Form.Label>
{isRequiredNameError && (
<Form.Message className="text-[13px] text-white opacity-[0.8]">
Please enter a name
</Form.Message>
)}
</div>
<Form.Control asChild>
<TextField.Root placeholder="Kitty" name="name" type="text" />
</Form.Control>
</Form.Field>
<Form.Field className="grid mb-[10px]" name="question">
<div className="flex items-baseline justify-between">
<Form.Label className="text-[15px] font-medium leading-[35px] text-white">
Category
</Form.Label>
<Form.Message
className="text-[13px] text-white opacity-[0.8]"
match="valueMissing"
>
Please enter a category
</Form.Message>
</div>
<Form.Control asChild>
<TextField.Root
placeholder="Cats"
name="category"
type="text"
required
/>
</Form.Control>
</Form.Field>
<Flex gapX="2">
<Form.Submit asChild>
<Button type="submit">
<PlusIcon /> Add Pet
</Button>
</Form.Submit>
<Button onClick={onUpdatePet} type="button">
<ReloadIcon /> Update Pet
</Button>
</Flex>
</Form.Root>
</Flex>
<Section size="1" />
</Container>
</Box>
);
}

export default App;
27 changes: 27 additions & 0 deletions examples/openapi-ts-ky/src/client/client.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// This file is auto-generated by @hey-api/openapi-ts

import {
type ClientOptions,
type Config,
createClient,
createConfig,
} from './client';
import type { ClientOptions as ClientOptions2 } from './types.gen';

/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
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',
}),
);
Loading
Loading