diff --git a/dev/openapi-ts.config.ts b/dev/openapi-ts.config.ts
index d4cc848c61..a9170ab8b9 100644
--- a/dev/openapi-ts.config.ts
+++ b/dev/openapi-ts.config.ts
@@ -41,9 +41,9 @@ export default defineConfig(() => {
// 'dutchie.json',
// 'invalid',
// 'full.yaml',
- // 'openai.yaml',
+ 'openai.yaml',
// 'opencode.yaml',
- 'pagination-ref.yaml',
+ // 'pagination-ref.yaml',
// 'sdk-instance.yaml',
// 'string-with-format.yaml',
// 'transformers.json',
diff --git a/examples/openapi-ts-axios/src/client/client/client.gen.ts b/examples/openapi-ts-axios/src/client/client/client.gen.ts
index f81a9e7863..b0628d02ab 100644
--- a/examples/openapi-ts-axios/src/client/client/client.gen.ts
+++ b/examples/openapi-ts-axios/src/client/client/client.gen.ts
@@ -76,6 +76,7 @@ export const createClient = (config: Config = {}): Client => {
try {
// assign Axios here for consistency with fetch
const _axios = opts.axios!;
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { auth, ...optsWithoutAuth } = opts;
const response = await _axios({
diff --git a/examples/openapi-ts-fastify/src/handlers.ts b/examples/openapi-ts-fastify/src/handlers.ts
index c4d94692ad..baad0a9a22 100644
--- a/examples/openapi-ts-fastify/src/handlers.ts
+++ b/examples/openapi-ts-fastify/src/handlers.ts
@@ -4,6 +4,7 @@ export const serviceHandlers: RouteHandlers = {
createPets(request, reply) {
reply.code(201).send();
},
+
listPets(request, reply) {
reply.code(200).send([]);
},
diff --git a/examples/openapi-ts-swr/.gitignore b/examples/openapi-ts-swr/.gitignore
new file mode 100644
index 0000000000..a547bf36d8
--- /dev/null
+++ b/examples/openapi-ts-swr/.gitignore
@@ -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?
diff --git a/examples/openapi-ts-swr/index.html b/examples/openapi-ts-swr/index.html
new file mode 100644
index 0000000000..b1cc18d0b6
--- /dev/null
+++ b/examples/openapi-ts-swr/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Hey API + SWR Demo
+
+
+
+
+
+
diff --git a/examples/openapi-ts-swr/openapi-ts.config.ts b/examples/openapi-ts-swr/openapi-ts.config.ts
new file mode 100644
index 0000000000..4ecd69aad8
--- /dev/null
+++ b/examples/openapi-ts-swr/openapi-ts.config.ts
@@ -0,0 +1,21 @@
+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-fetch',
+ '@hey-api/schemas',
+ '@hey-api/sdk',
+ {
+ enums: 'javascript',
+ name: '@hey-api/typescript',
+ },
+ 'swr',
+ ],
+});
diff --git a/examples/openapi-ts-swr/package.json b/examples/openapi-ts-swr/package.json
new file mode 100644
index 0000000000..1f74261914
--- /dev/null
+++ b/examples/openapi-ts-swr/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@example/openapi-ts-swr",
+ "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",
+ "react": "19.0.0",
+ "react-dom": "19.0.0",
+ "swr": "2.3.2"
+ },
+ "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"
+ }
+}
diff --git a/examples/openapi-ts-swr/postcss.config.js b/examples/openapi-ts-swr/postcss.config.js
new file mode 100644
index 0000000000..9eef821c48
--- /dev/null
+++ b/examples/openapi-ts-swr/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ autoprefixer: {},
+ tailwindcss: {},
+ },
+};
diff --git a/examples/openapi-ts-swr/src/App.css b/examples/openapi-ts-swr/src/App.css
new file mode 100644
index 0000000000..b5c61c9567
--- /dev/null
+++ b/examples/openapi-ts-swr/src/App.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/examples/openapi-ts-swr/src/App.tsx b/examples/openapi-ts-swr/src/App.tsx
new file mode 100644
index 0000000000..5613dfb393
--- /dev/null
+++ b/examples/openapi-ts-swr/src/App.tsx
@@ -0,0 +1,465 @@
+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 { useEffect, useState } from 'react';
+import useSWR from 'swr';
+// import useSWRInfinite from 'swr/infinite';
+import useSWRMutation from 'swr/mutation';
+
+import { createClient } from './client/client';
+import { PetSchema } from './client/schemas.gen';
+import { getInventory } from './client/sdk.gen';
+import {
+ addPetMutation,
+ findPetsByStatusKey,
+ findPetsByStatusOptions,
+ getInventoryKey,
+ getPetByIdOptions,
+ loginUserKey,
+ updatePetMutation,
+} from './client/swr.gen';
+// import { getPetByIdKey } from './client/swr.gen'; // For Pattern 2 example
+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 ',
+ },
+});
+
+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 ');
+ }
+ return request;
+});
+
+function App() {
+ const [pet, setPet] = useState();
+ const [petId, setPetId] = useState();
+ const [isRequiredNameError, setIsRequiredNameError] = useState(false);
+ const [showAdvancedExamples, setShowAdvancedExamples] = useState(false);
+
+ // ============================================================================
+ // Mutations - using the generated mutation options
+ // ============================================================================
+ // The mutation options provide the key and fetcher following SWR best practices
+ const { fetcher: addPetFetcher, key: addPetKey } = addPetMutation();
+ const addPet = useSWRMutation(addPetKey, addPetFetcher, {
+ onError: (error) => {
+ console.log(error);
+ setIsRequiredNameError(false);
+ },
+ onSuccess: (data) => {
+ setPet(data);
+ setIsRequiredNameError(false);
+ },
+ });
+
+ const { fetcher: updatePetFetcher, key: updatePetKey } = updatePetMutation();
+ const updatePet = useSWRMutation(updatePetKey, updatePetFetcher, {
+ onError: (error) => {
+ console.log(error);
+ },
+ onSuccess: (data) => {
+ setPet(data);
+ },
+ });
+
+ // ============================================================================
+ // Pattern 1: Using Options (Recommended for most cases)
+ // ============================================================================
+ // The options provide both key and fetcher in the correct format
+ // Conditional fetching is controlled by passing null to useSWR
+ const petOptions = petId
+ ? getPetByIdOptions({
+ client: localClient,
+ path: {
+ petId: petId!,
+ },
+ })
+ : null;
+
+ const { data, error } = useSWR(
+ petOptions?.key ?? null,
+ petOptions?.fetcher ?? null,
+ );
+
+ // ============================================================================
+ // Pattern 2: Using Key function directly (for custom fetchers)
+ // ============================================================================
+ // Key functions always return a valid key array, never null
+ // This gives you full control over the fetcher while maintaining cache consistency
+ //
+ // Example (disabled to avoid duplicate requests):
+ // const petByIdKey = petId ? getPetByIdKey({ path: { petId } }) : null;
+ // const { data: customFetchedPet } = useSWR(petByIdKey, async (key) => {
+ // if (!key) return null;
+ // // Custom fetch logic here - you can add transforms, error handling, etc.
+ // console.log('Fetching with key:', key);
+ // const response = await fetch(`/api/pet/${key[1]}`);
+ // return response.json();
+ // });
+
+ // ============================================================================
+ // Pattern 3: Optional parameters with optional chaining
+ // ============================================================================
+ // When options are optional, keys use optional chaining (options?.query)
+ // This is safe and always returns a valid key
+ const inventoryKey = getInventoryKey(); // No params needed
+ const { data: inventory } = useSWR(
+ showAdvancedExamples ? inventoryKey : null,
+ async () => {
+ // Custom fetcher - you control the implementation
+ const { data } = await getInventory({
+ client: localClient,
+ throwOnError: true,
+ });
+ return data;
+ },
+ );
+
+ // ============================================================================
+ // Pattern 4: Required parameters
+ // ============================================================================
+ // When parameters are required, options must be provided
+ // The key function directly accesses options.query without optional chaining
+ const petsByStatusKey = findPetsByStatusKey({
+ query: { status: 'available' },
+ });
+
+ // Or use the full options for convenience
+ const { fetcher: petsByStatusFetcher, key: petsByStatusKey2 } =
+ findPetsByStatusOptions({
+ client: localClient,
+ query: { status: 'available' },
+ });
+
+ const { data: availablePets } = useSWR(
+ showAdvancedExamples ? petsByStatusKey2 : null,
+ showAdvancedExamples ? petsByStatusFetcher : null,
+ );
+
+ // ============================================================================
+ // Pattern 5: Demonstrating key equality for cache consistency
+ // ============================================================================
+ // Keys with the same parameters will have the same cache entry
+ // This is a core SWR v2 improvement - primitive values in key arrays
+ const loginKey1 = loginUserKey({
+ query: { password: 'pass', username: 'test' },
+ });
+ const loginKey2 = loginUserKey({
+ query: { password: 'pass', username: 'test' },
+ });
+ // loginKey1 and loginKey2 will be treated as the same cache key by SWR
+ // because they have the same primitive values: ['/user/login', { username: 'test', password: 'pass' }]
+
+ const onAddPet = async (formData: FormData) => {
+ // simple form field validation to demonstrate using schemas
+ if (PetSchema.required.includes('name') && !formData.get('name')) {
+ setIsRequiredNameError(true);
+ return;
+ }
+
+ addPet.trigger({
+ 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',
+ },
+ ],
+ },
+ });
+ };
+
+ const onGetPetById = async () => {
+ // random id 1-10
+ setPetId(Math.floor(Math.random() * (10 - 1 + 1) + 1));
+ };
+
+ const onUpdatePet = async () => {
+ updatePet.trigger({
+ 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 ',
+ },
+ });
+ };
+
+ useEffect(() => {
+ if (error) {
+ console.log(error);
+ return;
+ }
+ setPet(data!);
+ }, [data, error]);
+
+ return (
+
+
+
+
+
+
+
+ @hey-api/openapi-ts 🤝 SWR
+
+
+
+ {/* Main Demo Section */}
+
+ Basic Usage Demo
+
+
+
+
+
+
+ Name: {pet?.name ?? 'N/A'}
+
+
+ Category: {pet?.category?.name ?? 'N/A'}
+
+
+
+
+
+
+
+
+
+
+ {/* Advanced Examples Toggle */}
+
+
+
+ {showAdvancedExamples && (
+
+
+ SWR v2 Key Patterns
+
+
+
+ Inventory (Optional params):
+
+
+ Key: {JSON.stringify(inventoryKey)}
+
+
+ Count:{' '}
+ {inventory ? Object.keys(inventory).length : 'Loading...'}
+
+
+
+
+
+ Available Pets (Required params):
+
+
+ Key: {JSON.stringify(petsByStatusKey)}
+
+
+ Found: {availablePets?.length ?? 'Loading...'} pets
+
+
+
+
+
+ Key Equality Demo:
+
+
+ Key 1: {JSON.stringify(loginKey1)}
+
+
+ Key 2: {JSON.stringify(loginKey2)}
+
+
+ ✓ These keys are equal and share the same cache
+
+
+
+
+ )}
+
+
+
+ {
+ event.preventDefault();
+ onAddPet(new FormData(event.currentTarget));
+ }}
+ >
+
+
+
+ Name
+
+ {isRequiredNameError && (
+
+ Please enter a name
+
+ )}
+
+
+
+
+
+
+
+
+ Category
+
+
+ Please enter a category
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/*
+ useSWRInfinite Example (for paginated endpoints):
+
+ If your OpenAPI spec has pagination configured, the SWR plugin generates
+ infinite options functions (e.g., findPetsByStatusInfinite).
+
+ These functions return an object with:
+ - getKey: Function that generates keys for each page
+ - fetcher: Function that fetches a single page
+
+ Example usage:
+
+ import useSWRInfinite from 'swr/infinite';
+ import { findPetsByStatusInfinite } from './client/swr.gen';
+
+ function InfinitePetList() {
+ // Get the infinite options with your query parameters
+ const { getKey, fetcher } = findPetsByStatusInfinite({
+ query: { status: 'available' }
+ });
+
+ const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher);
+
+ const pets = data ? data.flat() : [];
+ const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined');
+ const isEmpty = data?.[0]?.length === 0;
+ const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 20);
+
+ return (
+
+ {pets.map((pet) => (
+
+ {pet.name}
+
+ ))}
+
+
+ );
+ }
+
+ Note: The infinite options are only generated for operations that have
+ pagination configured in the OpenAPI spec.
+ */}
+
+
+ );
+}
+
+export default App;
diff --git a/examples/openapi-ts-swr/src/client/client.gen.ts b/examples/openapi-ts-swr/src/client/client.gen.ts
new file mode 100644
index 0000000000..069f4dabab
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/client.gen.ts
@@ -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 = (
+ override?: Config,
+) => Config & T>;
+
+export const client = createClient(
+ createConfig({
+ baseUrl: 'https://petstore3.swagger.io/api/v3',
+ }),
+);
diff --git a/examples/openapi-ts-swr/src/client/client/client.gen.ts b/examples/openapi-ts-swr/src/client/client/client.gen.ts
new file mode 100644
index 0000000000..c2a5190c22
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/client/client.gen.ts
@@ -0,0 +1,301 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import { createSseClient } from '../core/serverSentEvents.gen';
+import type { HttpMethod } from '../core/types.gen';
+import { getValidRequestBody } from '../core/utils.gen';
+import type {
+ Client,
+ Config,
+ RequestOptions,
+ ResolvedRequestOptions,
+} from './types.gen';
+import {
+ buildUrl,
+ createConfig,
+ createInterceptors,
+ getParseAs,
+ mergeConfigs,
+ mergeHeaders,
+ setAuthParams,
+} from './utils.gen';
+
+type ReqInit = Omit & {
+ body?: any;
+ headers: ReturnType;
+};
+
+export const createClient = (config: Config = {}): Client => {
+ let _config = mergeConfigs(createConfig(), config);
+
+ const getConfig = (): Config => ({ ..._config });
+
+ const setConfig = (config: Config): Config => {
+ _config = mergeConfigs(_config, config);
+ return getConfig();
+ };
+
+ const interceptors = createInterceptors<
+ Request,
+ Response,
+ unknown,
+ ResolvedRequestOptions
+ >();
+
+ const beforeRequest = async (options: RequestOptions) => {
+ const opts = {
+ ..._config,
+ ...options,
+ fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
+ headers: mergeHeaders(_config.headers, options.headers),
+ serializedBody: undefined,
+ };
+
+ if (opts.security) {
+ await setAuthParams({
+ ...opts,
+ security: opts.security,
+ });
+ }
+
+ if (opts.requestValidator) {
+ await opts.requestValidator(opts);
+ }
+
+ if (opts.body !== undefined && opts.bodySerializer) {
+ opts.serializedBody = opts.bodySerializer(opts.body);
+ }
+
+ // remove Content-Type header if body is empty to avoid sending invalid requests
+ if (opts.body === undefined || opts.serializedBody === '') {
+ opts.headers.delete('Content-Type');
+ }
+
+ const url = buildUrl(opts);
+
+ return { opts, url };
+ };
+
+ const request: Client['request'] = async (options) => {
+ // @ts-expect-error
+ const { opts, url } = await beforeRequest(options);
+ const requestInit: ReqInit = {
+ redirect: 'follow',
+ ...opts,
+ body: getValidRequestBody(opts),
+ };
+
+ let request = new Request(url, requestInit);
+
+ for (const fn of interceptors.request.fns) {
+ if (fn) {
+ request = await fn(request, opts);
+ }
+ }
+
+ // fetch must be assigned here, otherwise it would throw the error:
+ // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
+ const _fetch = opts.fetch!;
+ let response: Response;
+
+ try {
+ response = await _fetch(request);
+ } catch (error) {
+ // Handle fetch exceptions (AbortError, network errors, etc.)
+ let finalError = error;
+
+ for (const fn of interceptors.error.fns) {
+ if (fn) {
+ finalError = (await fn(
+ error,
+ undefined as any,
+ request,
+ opts,
+ )) as unknown;
+ }
+ }
+
+ finalError = finalError || ({} as unknown);
+
+ if (opts.throwOnError) {
+ throw finalError;
+ }
+
+ // Return error response
+ return opts.responseStyle === 'data'
+ ? undefined
+ : {
+ error: finalError,
+ request,
+ response: undefined as any,
+ };
+ }
+
+ for (const fn of interceptors.response.fns) {
+ if (fn) {
+ response = await fn(response, request, opts);
+ }
+ }
+
+ const result = {
+ request,
+ response,
+ };
+
+ if (response.ok) {
+ const parseAs =
+ (opts.parseAs === 'auto'
+ ? getParseAs(response.headers.get('Content-Type'))
+ : opts.parseAs) ?? 'json';
+
+ if (
+ response.status === 204 ||
+ response.headers.get('Content-Length') === '0'
+ ) {
+ let emptyData: any;
+ switch (parseAs) {
+ case 'arrayBuffer':
+ case 'blob':
+ case 'text':
+ emptyData = await response[parseAs]();
+ break;
+ case 'formData':
+ emptyData = new FormData();
+ break;
+ case 'stream':
+ emptyData = response.body;
+ break;
+ case 'json':
+ default:
+ emptyData = {};
+ break;
+ }
+ return opts.responseStyle === 'data'
+ ? emptyData
+ : {
+ data: emptyData,
+ ...result,
+ };
+ }
+
+ let data: any;
+ switch (parseAs) {
+ case 'arrayBuffer':
+ case 'blob':
+ case 'formData':
+ case 'json':
+ case 'text':
+ data = await response[parseAs]();
+ break;
+ case 'stream':
+ return opts.responseStyle === 'data'
+ ? response.body
+ : {
+ data: response.body,
+ ...result,
+ };
+ }
+
+ if (parseAs === 'json') {
+ if (opts.responseValidator) {
+ await opts.responseValidator(data);
+ }
+
+ if (opts.responseTransformer) {
+ data = await opts.responseTransformer(data);
+ }
+ }
+
+ return opts.responseStyle === 'data'
+ ? data
+ : {
+ data,
+ ...result,
+ };
+ }
+
+ const textError = await response.text();
+ let jsonError: unknown;
+
+ try {
+ jsonError = JSON.parse(textError);
+ } catch {
+ // noop
+ }
+
+ const error = jsonError ?? textError;
+ let finalError = error;
+
+ for (const fn of interceptors.error.fns) {
+ if (fn) {
+ finalError = (await fn(error, response, request, opts)) as string;
+ }
+ }
+
+ finalError = finalError || ({} as string);
+
+ if (opts.throwOnError) {
+ throw finalError;
+ }
+
+ // TODO: we probably want to return error and improve types
+ return opts.responseStyle === 'data'
+ ? undefined
+ : {
+ error: finalError,
+ ...result,
+ };
+ };
+
+ const makeMethodFn =
+ (method: Uppercase) => (options: RequestOptions) =>
+ request({ ...options, method });
+
+ const makeSseFn =
+ (method: Uppercase) => async (options: RequestOptions) => {
+ const { opts, url } = await beforeRequest(options);
+ return createSseClient({
+ ...opts,
+ body: opts.body as BodyInit | null | undefined,
+ headers: opts.headers as unknown as Record,
+ method,
+ onRequest: async (url, init) => {
+ let request = new Request(url, init);
+ for (const fn of interceptors.request.fns) {
+ if (fn) {
+ request = await fn(request, opts);
+ }
+ }
+ return request;
+ },
+ url,
+ });
+ };
+
+ return {
+ buildUrl,
+ connect: makeMethodFn('CONNECT'),
+ delete: makeMethodFn('DELETE'),
+ get: makeMethodFn('GET'),
+ getConfig,
+ head: makeMethodFn('HEAD'),
+ interceptors,
+ options: makeMethodFn('OPTIONS'),
+ patch: makeMethodFn('PATCH'),
+ post: makeMethodFn('POST'),
+ put: makeMethodFn('PUT'),
+ request,
+ setConfig,
+ sse: {
+ connect: makeSseFn('CONNECT'),
+ delete: makeSseFn('DELETE'),
+ get: makeSseFn('GET'),
+ head: makeSseFn('HEAD'),
+ options: makeSseFn('OPTIONS'),
+ patch: makeSseFn('PATCH'),
+ post: makeSseFn('POST'),
+ put: makeSseFn('PUT'),
+ trace: makeSseFn('TRACE'),
+ },
+ trace: makeMethodFn('TRACE'),
+ } as Client;
+};
diff --git a/examples/openapi-ts-swr/src/client/client/index.ts b/examples/openapi-ts-swr/src/client/client/index.ts
new file mode 100644
index 0000000000..b295edeca0
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/client/index.ts
@@ -0,0 +1,25 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+export type { Auth } from '../core/auth.gen';
+export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
+export {
+ formDataBodySerializer,
+ jsonBodySerializer,
+ urlSearchParamsBodySerializer,
+} from '../core/bodySerializer.gen';
+export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
+export { createClient } from './client.gen';
+export type {
+ Client,
+ ClientOptions,
+ Config,
+ CreateClientConfig,
+ Options,
+ RequestOptions,
+ RequestResult,
+ ResolvedRequestOptions,
+ ResponseStyle,
+ TDataShape,
+} from './types.gen';
+export { createConfig, mergeHeaders } from './utils.gen';
diff --git a/examples/openapi-ts-swr/src/client/client/types.gen.ts b/examples/openapi-ts-swr/src/client/client/types.gen.ts
new file mode 100644
index 0000000000..b4a499cc03
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/client/types.gen.ts
@@ -0,0 +1,241 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type { Auth } from '../core/auth.gen';
+import type {
+ ServerSentEventsOptions,
+ ServerSentEventsResult,
+} from '../core/serverSentEvents.gen';
+import type {
+ Client as CoreClient,
+ Config as CoreConfig,
+} from '../core/types.gen';
+import type { Middleware } from './utils.gen';
+
+export type ResponseStyle = 'data' | 'fields';
+
+export interface Config
+ extends Omit,
+ CoreConfig {
+ /**
+ * Base URL for all requests made by this client.
+ */
+ baseUrl?: T['baseUrl'];
+ /**
+ * Fetch API implementation. You can use this option to provide a custom
+ * fetch instance.
+ *
+ * @default globalThis.fetch
+ */
+ fetch?: typeof fetch;
+ /**
+ * Please don't use the Fetch client for Next.js applications. The `next`
+ * options won't have any effect.
+ *
+ * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
+ */
+ next?: never;
+ /**
+ * Return the response data parsed in a specified format. By default, `auto`
+ * will infer the appropriate method from the `Content-Type` response header.
+ * You can override this behavior with any of the {@link Body} methods.
+ * Select `stream` if you don't want to parse response data at all.
+ *
+ * @default 'auto'
+ */
+ parseAs?:
+ | 'arrayBuffer'
+ | 'auto'
+ | 'blob'
+ | 'formData'
+ | 'json'
+ | 'stream'
+ | 'text';
+ /**
+ * Should we return only data or multiple fields (data, error, response, etc.)?
+ *
+ * @default 'fields'
+ */
+ responseStyle?: ResponseStyle;
+ /**
+ * Throw an error instead of returning it in the response?
+ *
+ * @default false
+ */
+ throwOnError?: T['throwOnError'];
+}
+
+export interface RequestOptions<
+ TData = unknown,
+ TResponseStyle extends ResponseStyle = 'fields',
+ ThrowOnError extends boolean = boolean,
+ Url extends string = string,
+> extends Config<{
+ responseStyle: TResponseStyle;
+ throwOnError: ThrowOnError;
+ }>,
+ Pick<
+ ServerSentEventsOptions,
+ | 'onSseError'
+ | 'onSseEvent'
+ | 'sseDefaultRetryDelay'
+ | 'sseMaxRetryAttempts'
+ | 'sseMaxRetryDelay'
+ > {
+ /**
+ * Any body that you want to add to your request.
+ *
+ * {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
+ */
+ body?: unknown;
+ path?: Record;
+ query?: Record;
+ /**
+ * Security mechanism(s) to use for the request.
+ */
+ security?: ReadonlyArray;
+ url: Url;
+}
+
+export interface ResolvedRequestOptions<
+ TResponseStyle extends ResponseStyle = 'fields',
+ ThrowOnError extends boolean = boolean,
+ Url extends string = string,
+> extends RequestOptions {
+ serializedBody?: string;
+}
+
+export type RequestResult<
+ TData = unknown,
+ TError = unknown,
+ ThrowOnError extends boolean = boolean,
+ TResponseStyle extends ResponseStyle = 'fields',
+> = ThrowOnError extends true
+ ? Promise<
+ TResponseStyle extends 'data'
+ ? TData extends Record
+ ? TData[keyof TData]
+ : TData
+ : {
+ data: TData extends Record
+ ? TData[keyof TData]
+ : TData;
+ request: Request;
+ response: Response;
+ }
+ >
+ : Promise<
+ TResponseStyle extends 'data'
+ ?
+ | (TData extends Record
+ ? TData[keyof TData]
+ : TData)
+ | undefined
+ : (
+ | {
+ data: TData extends Record
+ ? TData[keyof TData]
+ : TData;
+ error: undefined;
+ }
+ | {
+ data: undefined;
+ error: TError extends Record
+ ? TError[keyof TError]
+ : TError;
+ }
+ ) & {
+ request: Request;
+ response: Response;
+ }
+ >;
+
+export interface ClientOptions {
+ baseUrl?: string;
+ responseStyle?: ResponseStyle;
+ throwOnError?: boolean;
+}
+
+type MethodFn = <
+ TData = unknown,
+ TError = unknown,
+ ThrowOnError extends boolean = false,
+ TResponseStyle extends ResponseStyle = 'fields',
+>(
+ options: Omit, 'method'>,
+) => RequestResult;
+
+type SseFn = <
+ TData = unknown,
+ TError = unknown,
+ ThrowOnError extends boolean = false,
+ TResponseStyle extends ResponseStyle = 'fields',
+>(
+ options: Omit, 'method'>,
+) => Promise>;
+
+type RequestFn = <
+ TData = unknown,
+ TError = unknown,
+ ThrowOnError extends boolean = false,
+ TResponseStyle extends ResponseStyle = 'fields',
+>(
+ options: Omit, 'method'> &
+ Pick<
+ Required>,
+ 'method'
+ >,
+) => RequestResult;
+
+type BuildUrlFn = <
+ TData extends {
+ body?: unknown;
+ path?: Record;
+ query?: Record;
+ url: string;
+ },
+>(
+ options: TData & Options,
+) => string;
+
+export type Client = CoreClient<
+ RequestFn,
+ Config,
+ MethodFn,
+ BuildUrlFn,
+ SseFn
+> & {
+ interceptors: Middleware;
+};
+
+/**
+ * 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 = (
+ override?: Config,
+) => Config & T>;
+
+export interface TDataShape {
+ body?: unknown;
+ headers?: unknown;
+ path?: unknown;
+ query?: unknown;
+ url: string;
+}
+
+type OmitKeys = Pick>;
+
+export type Options<
+ TData extends TDataShape = TDataShape,
+ ThrowOnError extends boolean = boolean,
+ TResponse = unknown,
+ TResponseStyle extends ResponseStyle = 'fields',
+> = OmitKeys<
+ RequestOptions,
+ 'body' | 'path' | 'query' | 'url'
+> &
+ ([TData] extends [never] ? unknown : Omit);
diff --git a/examples/openapi-ts-swr/src/client/client/utils.gen.ts b/examples/openapi-ts-swr/src/client/client/utils.gen.ts
new file mode 100644
index 0000000000..89416454b6
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/client/utils.gen.ts
@@ -0,0 +1,337 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import { getAuthToken } from '../core/auth.gen';
+import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
+import { jsonBodySerializer } from '../core/bodySerializer.gen';
+import {
+ serializeArrayParam,
+ serializeObjectParam,
+ serializePrimitiveParam,
+} from '../core/pathSerializer.gen';
+import { getUrl } from '../core/utils.gen';
+import type {
+ Client,
+ ClientOptions,
+ Config,
+ RequestOptions,
+} from './types.gen';
+
+export const createQuerySerializer = ({
+ parameters = {},
+ ...args
+}: QuerySerializerOptions = {}) => {
+ const querySerializer = (queryParams: T) => {
+ const search: string[] = [];
+ if (queryParams && typeof queryParams === 'object') {
+ for (const name in queryParams) {
+ const value = queryParams[name];
+
+ if (value === undefined || value === null) {
+ continue;
+ }
+
+ const options = parameters[name] || args;
+
+ if (Array.isArray(value)) {
+ const serializedArray = serializeArrayParam({
+ allowReserved: options.allowReserved,
+ explode: true,
+ name,
+ style: 'form',
+ value,
+ ...options.array,
+ });
+ if (serializedArray) search.push(serializedArray);
+ } else if (typeof value === 'object') {
+ const serializedObject = serializeObjectParam({
+ allowReserved: options.allowReserved,
+ explode: true,
+ name,
+ style: 'deepObject',
+ value: value as Record,
+ ...options.object,
+ });
+ if (serializedObject) search.push(serializedObject);
+ } else {
+ const serializedPrimitive = serializePrimitiveParam({
+ allowReserved: options.allowReserved,
+ name,
+ value: value as string,
+ });
+ if (serializedPrimitive) search.push(serializedPrimitive);
+ }
+ }
+ }
+ return search.join('&');
+ };
+ return querySerializer;
+};
+
+/**
+ * Infers parseAs value from provided Content-Type header.
+ */
+export const getParseAs = (
+ contentType: string | null,
+): Exclude => {
+ if (!contentType) {
+ // If no Content-Type header is provided, the best we can do is return the raw response body,
+ // which is effectively the same as the 'stream' option.
+ return 'stream';
+ }
+
+ const cleanContent = contentType.split(';')[0]?.trim();
+
+ if (!cleanContent) {
+ return;
+ }
+
+ if (
+ cleanContent.startsWith('application/json') ||
+ cleanContent.endsWith('+json')
+ ) {
+ return 'json';
+ }
+
+ if (cleanContent === 'multipart/form-data') {
+ return 'formData';
+ }
+
+ if (
+ ['application/', 'audio/', 'image/', 'video/'].some((type) =>
+ cleanContent.startsWith(type),
+ )
+ ) {
+ return 'blob';
+ }
+
+ if (cleanContent.startsWith('text/')) {
+ return 'text';
+ }
+
+ return;
+};
+
+const checkForExistence = (
+ options: Pick & {
+ headers: Headers;
+ },
+ name?: string,
+): boolean => {
+ if (!name) {
+ return false;
+ }
+ if (
+ options.headers.has(name) ||
+ options.query?.[name] ||
+ options.headers.get('Cookie')?.includes(`${name}=`)
+ ) {
+ return true;
+ }
+ return false;
+};
+
+export const setAuthParams = async ({
+ security,
+ ...options
+}: Pick, 'security'> &
+ Pick & {
+ headers: Headers;
+ }) => {
+ for (const auth of security) {
+ if (checkForExistence(options, auth.name)) {
+ continue;
+ }
+
+ const token = await getAuthToken(auth, options.auth);
+
+ if (!token) {
+ continue;
+ }
+
+ const name = auth.name ?? 'Authorization';
+
+ switch (auth.in) {
+ case 'query':
+ if (!options.query) {
+ options.query = {};
+ }
+ options.query[name] = token;
+ break;
+ case 'cookie':
+ options.headers.append('Cookie', `${name}=${token}`);
+ break;
+ case 'header':
+ default:
+ options.headers.set(name, token);
+ break;
+ }
+ }
+};
+
+export const buildUrl: Client['buildUrl'] = (options) =>
+ getUrl({
+ baseUrl: options.baseUrl as string,
+ path: options.path,
+ query: options.query,
+ querySerializer:
+ typeof options.querySerializer === 'function'
+ ? options.querySerializer
+ : createQuerySerializer(options.querySerializer),
+ url: options.url,
+ });
+
+export const mergeConfigs = (a: Config, b: Config): Config => {
+ const config = { ...a, ...b };
+ if (config.baseUrl?.endsWith('/')) {
+ config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
+ }
+ config.headers = mergeHeaders(a.headers, b.headers);
+ return config;
+};
+
+const headersEntries = (headers: Headers): Array<[string, string]> => {
+ const entries: Array<[string, string]> = [];
+ headers.forEach((value, key) => {
+ entries.push([key, value]);
+ });
+ return entries;
+};
+
+export const mergeHeaders = (
+ ...headers: Array['headers'] | undefined>
+): Headers => {
+ const mergedHeaders = new Headers();
+ for (const header of headers) {
+ if (!header) {
+ continue;
+ }
+
+ const iterator =
+ header instanceof Headers
+ ? headersEntries(header)
+ : Object.entries(header);
+
+ for (const [key, value] of iterator) {
+ if (value === null) {
+ mergedHeaders.delete(key);
+ } else if (Array.isArray(value)) {
+ for (const v of value) {
+ mergedHeaders.append(key, v as string);
+ }
+ } else if (value !== undefined) {
+ // assume object headers are meant to be JSON stringified, i.e. their
+ // content value in OpenAPI specification is 'application/json'
+ mergedHeaders.set(
+ key,
+ typeof value === 'object' ? JSON.stringify(value) : (value as string),
+ );
+ }
+ }
+ }
+ return mergedHeaders;
+};
+
+type ErrInterceptor = (
+ error: Err,
+ response: Res,
+ request: Req,
+ options: Options,
+) => Err | Promise;
+
+type ReqInterceptor = (
+ request: Req,
+ options: Options,
+) => Req | Promise;
+
+type ResInterceptor = (
+ response: Res,
+ request: Req,
+ options: Options,
+) => Res | Promise;
+
+class Interceptors {
+ fns: Array = [];
+
+ clear(): void {
+ this.fns = [];
+ }
+
+ eject(id: number | Interceptor): void {
+ const index = this.getInterceptorIndex(id);
+ if (this.fns[index]) {
+ this.fns[index] = null;
+ }
+ }
+
+ exists(id: number | Interceptor): boolean {
+ const index = this.getInterceptorIndex(id);
+ return Boolean(this.fns[index]);
+ }
+
+ getInterceptorIndex(id: number | Interceptor): number {
+ if (typeof id === 'number') {
+ return this.fns[id] ? id : -1;
+ }
+ return this.fns.indexOf(id);
+ }
+
+ update(
+ id: number | Interceptor,
+ fn: Interceptor,
+ ): number | Interceptor | false {
+ const index = this.getInterceptorIndex(id);
+ if (this.fns[index]) {
+ this.fns[index] = fn;
+ return id;
+ }
+ return false;
+ }
+
+ use(fn: Interceptor): number {
+ this.fns.push(fn);
+ return this.fns.length - 1;
+ }
+}
+
+export interface Middleware {
+ error: Interceptors>;
+ request: Interceptors>;
+ response: Interceptors>;
+}
+
+export const createInterceptors = (): Middleware<
+ Req,
+ Res,
+ Err,
+ Options
+> => ({
+ error: new Interceptors>(),
+ request: new Interceptors>(),
+ response: new Interceptors>(),
+});
+
+const defaultQuerySerializer = createQuerySerializer({
+ allowReserved: false,
+ array: {
+ explode: true,
+ style: 'form',
+ },
+ object: {
+ explode: true,
+ style: 'deepObject',
+ },
+});
+
+const defaultHeaders = {
+ 'Content-Type': 'application/json',
+};
+
+export const createConfig = (
+ override: Config & T> = {},
+): Config & T> => ({
+ ...jsonBodySerializer,
+ headers: defaultHeaders,
+ parseAs: 'auto',
+ querySerializer: defaultQuerySerializer,
+ ...override,
+});
diff --git a/examples/openapi-ts-swr/src/client/core/auth.gen.ts b/examples/openapi-ts-swr/src/client/core/auth.gen.ts
new file mode 100644
index 0000000000..f8a73266f9
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/core/auth.gen.ts
@@ -0,0 +1,42 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+export type AuthToken = string | undefined;
+
+export interface Auth {
+ /**
+ * Which part of the request do we use to send the auth?
+ *
+ * @default 'header'
+ */
+ in?: 'header' | 'query' | 'cookie';
+ /**
+ * Header or query parameter name.
+ *
+ * @default 'Authorization'
+ */
+ name?: string;
+ scheme?: 'basic' | 'bearer';
+ type: 'apiKey' | 'http';
+}
+
+export const getAuthToken = async (
+ auth: Auth,
+ callback: ((auth: Auth) => Promise | AuthToken) | AuthToken,
+): Promise => {
+ const token =
+ typeof callback === 'function' ? await callback(auth) : callback;
+
+ if (!token) {
+ return;
+ }
+
+ if (auth.scheme === 'bearer') {
+ return `Bearer ${token}`;
+ }
+
+ if (auth.scheme === 'basic') {
+ return `Basic ${btoa(token)}`;
+ }
+
+ return token;
+};
diff --git a/examples/openapi-ts-swr/src/client/core/bodySerializer.gen.ts b/examples/openapi-ts-swr/src/client/core/bodySerializer.gen.ts
new file mode 100644
index 0000000000..552b50f7c8
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/core/bodySerializer.gen.ts
@@ -0,0 +1,100 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type {
+ ArrayStyle,
+ ObjectStyle,
+ SerializerOptions,
+} from './pathSerializer.gen';
+
+export type QuerySerializer = (query: Record) => string;
+
+export type BodySerializer = (body: any) => any;
+
+type QuerySerializerOptionsObject = {
+ allowReserved?: boolean;
+ array?: Partial>;
+ object?: Partial>;
+};
+
+export type QuerySerializerOptions = QuerySerializerOptionsObject & {
+ /**
+ * Per-parameter serialization overrides. When provided, these settings
+ * override the global array/object settings for specific parameter names.
+ */
+ parameters?: Record;
+};
+
+const serializeFormDataPair = (
+ data: FormData,
+ key: string,
+ value: unknown,
+): void => {
+ if (typeof value === 'string' || value instanceof Blob) {
+ data.append(key, value);
+ } else if (value instanceof Date) {
+ data.append(key, value.toISOString());
+ } else {
+ data.append(key, JSON.stringify(value));
+ }
+};
+
+const serializeUrlSearchParamsPair = (
+ data: URLSearchParams,
+ key: string,
+ value: unknown,
+): void => {
+ if (typeof value === 'string') {
+ data.append(key, value);
+ } else {
+ data.append(key, JSON.stringify(value));
+ }
+};
+
+export const formDataBodySerializer = {
+ bodySerializer: | Array>>(
+ body: T,
+ ): FormData => {
+ const data = new FormData();
+
+ Object.entries(body).forEach(([key, value]) => {
+ if (value === undefined || value === null) {
+ return;
+ }
+ if (Array.isArray(value)) {
+ value.forEach((v) => serializeFormDataPair(data, key, v));
+ } else {
+ serializeFormDataPair(data, key, value);
+ }
+ });
+
+ return data;
+ },
+};
+
+export const jsonBodySerializer = {
+ bodySerializer: (body: T): string =>
+ JSON.stringify(body, (_key, value) =>
+ typeof value === 'bigint' ? value.toString() : value,
+ ),
+};
+
+export const urlSearchParamsBodySerializer = {
+ bodySerializer: | Array>>(
+ body: T,
+ ): string => {
+ const data = new URLSearchParams();
+
+ Object.entries(body).forEach(([key, value]) => {
+ if (value === undefined || value === null) {
+ return;
+ }
+ if (Array.isArray(value)) {
+ value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
+ } else {
+ serializeUrlSearchParamsPair(data, key, value);
+ }
+ });
+
+ return data.toString();
+ },
+};
diff --git a/examples/openapi-ts-swr/src/client/core/params.gen.ts b/examples/openapi-ts-swr/src/client/core/params.gen.ts
new file mode 100644
index 0000000000..602715c46c
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/core/params.gen.ts
@@ -0,0 +1,176 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+type Slot = 'body' | 'headers' | 'path' | 'query';
+
+export type Field =
+ | {
+ in: Exclude;
+ /**
+ * Field name. This is the name we want the user to see and use.
+ */
+ key: string;
+ /**
+ * Field mapped name. This is the name we want to use in the request.
+ * If omitted, we use the same value as `key`.
+ */
+ map?: string;
+ }
+ | {
+ in: Extract;
+ /**
+ * Key isn't required for bodies.
+ */
+ key?: string;
+ map?: string;
+ }
+ | {
+ /**
+ * Field name. This is the name we want the user to see and use.
+ */
+ key: string;
+ /**
+ * Field mapped name. This is the name we want to use in the request.
+ * If `in` is omitted, `map` aliases `key` to the transport layer.
+ */
+ map: Slot;
+ };
+
+export interface Fields {
+ allowExtra?: Partial>;
+ args?: ReadonlyArray;
+}
+
+export type FieldsConfig = ReadonlyArray;
+
+const extraPrefixesMap: Record = {
+ $body_: 'body',
+ $headers_: 'headers',
+ $path_: 'path',
+ $query_: 'query',
+};
+const extraPrefixes = Object.entries(extraPrefixesMap);
+
+type KeyMap = Map<
+ string,
+ | {
+ in: Slot;
+ map?: string;
+ }
+ | {
+ in?: never;
+ map: Slot;
+ }
+>;
+
+const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
+ if (!map) {
+ map = new Map();
+ }
+
+ for (const config of fields) {
+ if ('in' in config) {
+ if (config.key) {
+ map.set(config.key, {
+ in: config.in,
+ map: config.map,
+ });
+ }
+ } else if ('key' in config) {
+ map.set(config.key, {
+ map: config.map,
+ });
+ } else if (config.args) {
+ buildKeyMap(config.args, map);
+ }
+ }
+
+ return map;
+};
+
+interface Params {
+ body: unknown;
+ headers: Record;
+ path: Record;
+ query: Record;
+}
+
+const stripEmptySlots = (params: Params) => {
+ for (const [slot, value] of Object.entries(params)) {
+ if (value && typeof value === 'object' && !Object.keys(value).length) {
+ delete params[slot as Slot];
+ }
+ }
+};
+
+export const buildClientParams = (
+ args: ReadonlyArray,
+ fields: FieldsConfig,
+) => {
+ const params: Params = {
+ body: {},
+ headers: {},
+ path: {},
+ query: {},
+ };
+
+ const map = buildKeyMap(fields);
+
+ let config: FieldsConfig[number] | undefined;
+
+ for (const [index, arg] of args.entries()) {
+ if (fields[index]) {
+ config = fields[index];
+ }
+
+ if (!config) {
+ continue;
+ }
+
+ if ('in' in config) {
+ if (config.key) {
+ const field = map.get(config.key)!;
+ const name = field.map || config.key;
+ if (field.in) {
+ (params[field.in] as Record)[name] = arg;
+ }
+ } else {
+ params.body = arg;
+ }
+ } else {
+ for (const [key, value] of Object.entries(arg ?? {})) {
+ const field = map.get(key);
+
+ if (field) {
+ if (field.in) {
+ const name = field.map || key;
+ (params[field.in] as Record)[name] = value;
+ } else {
+ params[field.map] = value;
+ }
+ } else {
+ const extra = extraPrefixes.find(([prefix]) =>
+ key.startsWith(prefix),
+ );
+
+ if (extra) {
+ const [prefix, slot] = extra;
+ (params[slot] as Record)[
+ key.slice(prefix.length)
+ ] = value;
+ } else if ('allowExtra' in config && config.allowExtra) {
+ for (const [slot, allowed] of Object.entries(config.allowExtra)) {
+ if (allowed) {
+ (params[slot as Slot] as Record)[key] = value;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ stripEmptySlots(params);
+
+ return params;
+};
diff --git a/examples/openapi-ts-swr/src/client/core/pathSerializer.gen.ts b/examples/openapi-ts-swr/src/client/core/pathSerializer.gen.ts
new file mode 100644
index 0000000000..8d99931047
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/core/pathSerializer.gen.ts
@@ -0,0 +1,181 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+interface SerializeOptions
+ extends SerializePrimitiveOptions,
+ SerializerOptions {}
+
+interface SerializePrimitiveOptions {
+ allowReserved?: boolean;
+ name: string;
+}
+
+export interface SerializerOptions {
+ /**
+ * @default true
+ */
+ explode: boolean;
+ style: T;
+}
+
+export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
+export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
+type MatrixStyle = 'label' | 'matrix' | 'simple';
+export type ObjectStyle = 'form' | 'deepObject';
+type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
+
+interface SerializePrimitiveParam extends SerializePrimitiveOptions {
+ value: string;
+}
+
+export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
+ switch (style) {
+ case 'label':
+ return '.';
+ case 'matrix':
+ return ';';
+ case 'simple':
+ return ',';
+ default:
+ return '&';
+ }
+};
+
+export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
+ switch (style) {
+ case 'form':
+ return ',';
+ case 'pipeDelimited':
+ return '|';
+ case 'spaceDelimited':
+ return '%20';
+ default:
+ return ',';
+ }
+};
+
+export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
+ switch (style) {
+ case 'label':
+ return '.';
+ case 'matrix':
+ return ';';
+ case 'simple':
+ return ',';
+ default:
+ return '&';
+ }
+};
+
+export const serializeArrayParam = ({
+ allowReserved,
+ explode,
+ name,
+ style,
+ value,
+}: SerializeOptions & {
+ value: unknown[];
+}) => {
+ if (!explode) {
+ const joinedValues = (
+ allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
+ ).join(separatorArrayNoExplode(style));
+ switch (style) {
+ case 'label':
+ return `.${joinedValues}`;
+ case 'matrix':
+ return `;${name}=${joinedValues}`;
+ case 'simple':
+ return joinedValues;
+ default:
+ return `${name}=${joinedValues}`;
+ }
+ }
+
+ const separator = separatorArrayExplode(style);
+ const joinedValues = value
+ .map((v) => {
+ if (style === 'label' || style === 'simple') {
+ return allowReserved ? v : encodeURIComponent(v as string);
+ }
+
+ return serializePrimitiveParam({
+ allowReserved,
+ name,
+ value: v as string,
+ });
+ })
+ .join(separator);
+ return style === 'label' || style === 'matrix'
+ ? separator + joinedValues
+ : joinedValues;
+};
+
+export const serializePrimitiveParam = ({
+ allowReserved,
+ name,
+ value,
+}: SerializePrimitiveParam) => {
+ if (value === undefined || value === null) {
+ return '';
+ }
+
+ if (typeof value === 'object') {
+ throw new Error(
+ 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.',
+ );
+ }
+
+ return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
+};
+
+export const serializeObjectParam = ({
+ allowReserved,
+ explode,
+ name,
+ style,
+ value,
+ valueOnly,
+}: SerializeOptions & {
+ value: Record | Date;
+ valueOnly?: boolean;
+}) => {
+ if (value instanceof Date) {
+ return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
+ }
+
+ if (style !== 'deepObject' && !explode) {
+ let values: string[] = [];
+ Object.entries(value).forEach(([key, v]) => {
+ values = [
+ ...values,
+ key,
+ allowReserved ? (v as string) : encodeURIComponent(v as string),
+ ];
+ });
+ const joinedValues = values.join(',');
+ switch (style) {
+ case 'form':
+ return `${name}=${joinedValues}`;
+ case 'label':
+ return `.${joinedValues}`;
+ case 'matrix':
+ return `;${name}=${joinedValues}`;
+ default:
+ return joinedValues;
+ }
+ }
+
+ const separator = separatorObjectExplode(style);
+ const joinedValues = Object.entries(value)
+ .map(([key, v]) =>
+ serializePrimitiveParam({
+ allowReserved,
+ name: style === 'deepObject' ? `${name}[${key}]` : key,
+ value: v as string,
+ }),
+ )
+ .join(separator);
+ return style === 'label' || style === 'matrix'
+ ? separator + joinedValues
+ : joinedValues;
+};
diff --git a/examples/openapi-ts-swr/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-swr/src/client/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/examples/openapi-ts-swr/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-swr/src/client/core/serverSentEvents.gen.ts
new file mode 100644
index 0000000000..f8fd78e284
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/core/serverSentEvents.gen.ts
@@ -0,0 +1,264 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type { Config } from './types.gen';
+
+export type ServerSentEventsOptions = Omit<
+ RequestInit,
+ 'method'
+> &
+ Pick & {
+ /**
+ * Fetch API implementation. You can use this option to provide a custom
+ * fetch instance.
+ *
+ * @default globalThis.fetch
+ */
+ fetch?: typeof fetch;
+ /**
+ * Implementing clients can call request interceptors inside this hook.
+ */
+ onRequest?: (url: string, init: RequestInit) => Promise;
+ /**
+ * Callback invoked when a network or parsing error occurs during streaming.
+ *
+ * This option applies only if the endpoint returns a stream of events.
+ *
+ * @param error The error that occurred.
+ */
+ onSseError?: (error: unknown) => void;
+ /**
+ * Callback invoked when an event is streamed from the server.
+ *
+ * This option applies only if the endpoint returns a stream of events.
+ *
+ * @param event Event streamed from the server.
+ * @returns Nothing (void).
+ */
+ onSseEvent?: (event: StreamEvent) => void;
+ serializedBody?: RequestInit['body'];
+ /**
+ * Default retry delay in milliseconds.
+ *
+ * This option applies only if the endpoint returns a stream of events.
+ *
+ * @default 3000
+ */
+ sseDefaultRetryDelay?: number;
+ /**
+ * Maximum number of retry attempts before giving up.
+ */
+ sseMaxRetryAttempts?: number;
+ /**
+ * Maximum retry delay in milliseconds.
+ *
+ * Applies only when exponential backoff is used.
+ *
+ * This option applies only if the endpoint returns a stream of events.
+ *
+ * @default 30000
+ */
+ sseMaxRetryDelay?: number;
+ /**
+ * Optional sleep function for retry backoff.
+ *
+ * Defaults to using `setTimeout`.
+ */
+ sseSleepFn?: (ms: number) => Promise;
+ url: string;
+ };
+
+export interface StreamEvent {
+ data: TData;
+ event?: string;
+ id?: string;
+ retry?: number;
+}
+
+export type ServerSentEventsResult<
+ TData = unknown,
+ TReturn = void,
+ TNext = unknown,
+> = {
+ stream: AsyncGenerator<
+ TData extends Record ? TData[keyof TData] : TData,
+ TReturn,
+ TNext
+ >;
+};
+
+export const createSseClient = ({
+ onRequest,
+ onSseError,
+ onSseEvent,
+ responseTransformer,
+ responseValidator,
+ sseDefaultRetryDelay,
+ sseMaxRetryAttempts,
+ sseMaxRetryDelay,
+ sseSleepFn,
+ url,
+ ...options
+}: ServerSentEventsOptions): ServerSentEventsResult => {
+ let lastEventId: string | undefined;
+
+ const sleep =
+ sseSleepFn ??
+ ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
+
+ const createStream = async function* () {
+ let retryDelay: number = sseDefaultRetryDelay ?? 3000;
+ let attempt = 0;
+ const signal = options.signal ?? new AbortController().signal;
+
+ while (true) {
+ if (signal.aborted) break;
+
+ attempt++;
+
+ const headers =
+ options.headers instanceof Headers
+ ? options.headers
+ : new Headers(options.headers as Record | undefined);
+
+ if (lastEventId !== undefined) {
+ headers.set('Last-Event-ID', lastEventId);
+ }
+
+ try {
+ const requestInit: RequestInit = {
+ redirect: 'follow',
+ ...options,
+ body: options.serializedBody,
+ headers,
+ signal,
+ };
+ let request = new Request(url, requestInit);
+ if (onRequest) {
+ request = await onRequest(url, requestInit);
+ }
+ // fetch must be assigned here, otherwise it would throw the error:
+ // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
+ const _fetch = options.fetch ?? globalThis.fetch;
+ const response = await _fetch(request);
+
+ if (!response.ok)
+ throw new Error(
+ `SSE failed: ${response.status} ${response.statusText}`,
+ );
+
+ if (!response.body) throw new Error('No body in SSE response');
+
+ const reader = response.body
+ .pipeThrough(new TextDecoderStream())
+ .getReader();
+
+ let buffer = '';
+
+ const abortHandler = () => {
+ try {
+ reader.cancel();
+ } catch {
+ // noop
+ }
+ };
+
+ signal.addEventListener('abort', abortHandler);
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ buffer += value;
+
+ const chunks = buffer.split('\n\n');
+ buffer = chunks.pop() ?? '';
+
+ for (const chunk of chunks) {
+ const lines = chunk.split('\n');
+ const dataLines: Array = [];
+ let eventName: string | undefined;
+
+ for (const line of lines) {
+ if (line.startsWith('data:')) {
+ dataLines.push(line.replace(/^data:\s*/, ''));
+ } else if (line.startsWith('event:')) {
+ eventName = line.replace(/^event:\s*/, '');
+ } else if (line.startsWith('id:')) {
+ lastEventId = line.replace(/^id:\s*/, '');
+ } else if (line.startsWith('retry:')) {
+ const parsed = Number.parseInt(
+ line.replace(/^retry:\s*/, ''),
+ 10,
+ );
+ if (!Number.isNaN(parsed)) {
+ retryDelay = parsed;
+ }
+ }
+ }
+
+ let data: unknown;
+ let parsedJson = false;
+
+ if (dataLines.length) {
+ const rawData = dataLines.join('\n');
+ try {
+ data = JSON.parse(rawData);
+ parsedJson = true;
+ } catch {
+ data = rawData;
+ }
+ }
+
+ if (parsedJson) {
+ if (responseValidator) {
+ await responseValidator(data);
+ }
+
+ if (responseTransformer) {
+ data = await responseTransformer(data);
+ }
+ }
+
+ onSseEvent?.({
+ data,
+ event: eventName,
+ id: lastEventId,
+ retry: retryDelay,
+ });
+
+ if (dataLines.length) {
+ yield data as any;
+ }
+ }
+ }
+ } finally {
+ signal.removeEventListener('abort', abortHandler);
+ reader.releaseLock();
+ }
+
+ break; // exit loop on normal completion
+ } catch (error) {
+ // connection failed or aborted; retry after delay
+ onSseError?.(error);
+
+ if (
+ sseMaxRetryAttempts !== undefined &&
+ attempt >= sseMaxRetryAttempts
+ ) {
+ break; // stop after firing error
+ }
+
+ // exponential backoff: double retry each attempt, cap at 30s
+ const backoff = Math.min(
+ retryDelay * 2 ** (attempt - 1),
+ sseMaxRetryDelay ?? 30000,
+ );
+ await sleep(backoff);
+ }
+ }
+ };
+
+ const stream = createStream();
+
+ return { stream };
+};
diff --git a/examples/openapi-ts-swr/src/client/core/types.gen.ts b/examples/openapi-ts-swr/src/client/core/types.gen.ts
new file mode 100644
index 0000000000..643c070c9d
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/core/types.gen.ts
@@ -0,0 +1,118 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type { Auth, AuthToken } from './auth.gen';
+import type {
+ BodySerializer,
+ QuerySerializer,
+ QuerySerializerOptions,
+} from './bodySerializer.gen';
+
+export type HttpMethod =
+ | 'connect'
+ | 'delete'
+ | 'get'
+ | 'head'
+ | 'options'
+ | 'patch'
+ | 'post'
+ | 'put'
+ | 'trace';
+
+export type Client<
+ RequestFn = never,
+ Config = unknown,
+ MethodFn = never,
+ BuildUrlFn = never,
+ SseFn = never,
+> = {
+ /**
+ * Returns the final request URL.
+ */
+ buildUrl: BuildUrlFn;
+ getConfig: () => Config;
+ request: RequestFn;
+ setConfig: (config: Config) => Config;
+} & {
+ [K in HttpMethod]: MethodFn;
+} & ([SseFn] extends [never]
+ ? { sse?: never }
+ : { sse: { [K in HttpMethod]: SseFn } });
+
+export interface Config {
+ /**
+ * Auth token or a function returning auth token. The resolved value will be
+ * added to the request payload as defined by its `security` array.
+ */
+ auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken;
+ /**
+ * A function for serializing request body parameter. By default,
+ * {@link JSON.stringify()} will be used.
+ */
+ bodySerializer?: BodySerializer | null;
+ /**
+ * An object containing any HTTP headers that you want to pre-populate your
+ * `Headers` object with.
+ *
+ * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
+ */
+ headers?:
+ | RequestInit['headers']
+ | Record<
+ string,
+ | string
+ | number
+ | boolean
+ | (string | number | boolean)[]
+ | null
+ | undefined
+ | unknown
+ >;
+ /**
+ * The request method.
+ *
+ * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
+ */
+ method?: Uppercase;
+ /**
+ * A function for serializing request query parameters. By default, arrays
+ * will be exploded in form style, objects will be exploded in deepObject
+ * style, and reserved characters are percent-encoded.
+ *
+ * This method will have no effect if the native `paramsSerializer()` Axios
+ * API function is used.
+ *
+ * {@link https://swagger.io/docs/specification/serialization/#query View examples}
+ */
+ querySerializer?: QuerySerializer | QuerySerializerOptions;
+ /**
+ * A function validating request data. This is useful if you want to ensure
+ * the request conforms to the desired shape, so it can be safely sent to
+ * the server.
+ */
+ requestValidator?: (data: unknown) => Promise;
+ /**
+ * A function transforming response data before it's returned. This is useful
+ * for post-processing data, e.g. converting ISO strings into Date objects.
+ */
+ responseTransformer?: (data: unknown) => Promise;
+ /**
+ * A function validating response data. This is useful if you want to ensure
+ * the response conforms to the desired shape, so it can be safely passed to
+ * the transformers and returned to the user.
+ */
+ responseValidator?: (data: unknown) => Promise;
+}
+
+type IsExactlyNeverOrNeverUndefined = [T] extends [never]
+ ? true
+ : [T] extends [never | undefined]
+ ? [undefined] extends [T]
+ ? false
+ : true
+ : false;
+
+export type OmitNever> = {
+ [K in keyof T as IsExactlyNeverOrNeverUndefined extends true
+ ? never
+ : K]: T[K];
+};
diff --git a/examples/openapi-ts-swr/src/client/core/utils.gen.ts b/examples/openapi-ts-swr/src/client/core/utils.gen.ts
new file mode 100644
index 0000000000..0b5389d089
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/core/utils.gen.ts
@@ -0,0 +1,143 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
+import {
+ type ArraySeparatorStyle,
+ serializeArrayParam,
+ serializeObjectParam,
+ serializePrimitiveParam,
+} from './pathSerializer.gen';
+
+export interface PathSerializer {
+ path: Record;
+ url: string;
+}
+
+export const PATH_PARAM_RE = /\{[^{}]+\}/g;
+
+export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
+ let url = _url;
+ const matches = _url.match(PATH_PARAM_RE);
+ if (matches) {
+ for (const match of matches) {
+ let explode = false;
+ let name = match.substring(1, match.length - 1);
+ let style: ArraySeparatorStyle = 'simple';
+
+ if (name.endsWith('*')) {
+ explode = true;
+ name = name.substring(0, name.length - 1);
+ }
+
+ if (name.startsWith('.')) {
+ name = name.substring(1);
+ style = 'label';
+ } else if (name.startsWith(';')) {
+ name = name.substring(1);
+ style = 'matrix';
+ }
+
+ const value = path[name];
+
+ if (value === undefined || value === null) {
+ continue;
+ }
+
+ if (Array.isArray(value)) {
+ url = url.replace(
+ match,
+ serializeArrayParam({ explode, name, style, value }),
+ );
+ continue;
+ }
+
+ if (typeof value === 'object') {
+ url = url.replace(
+ match,
+ serializeObjectParam({
+ explode,
+ name,
+ style,
+ value: value as Record,
+ valueOnly: true,
+ }),
+ );
+ continue;
+ }
+
+ if (style === 'matrix') {
+ url = url.replace(
+ match,
+ `;${serializePrimitiveParam({
+ name,
+ value: value as string,
+ })}`,
+ );
+ continue;
+ }
+
+ const replaceValue = encodeURIComponent(
+ style === 'label' ? `.${value as string}` : (value as string),
+ );
+ url = url.replace(match, replaceValue);
+ }
+ }
+ return url;
+};
+
+export const getUrl = ({
+ baseUrl,
+ path,
+ query,
+ querySerializer,
+ url: _url,
+}: {
+ baseUrl?: string;
+ path?: Record;
+ query?: Record;
+ querySerializer: QuerySerializer;
+ url: string;
+}) => {
+ const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
+ let url = (baseUrl ?? '') + pathUrl;
+ if (path) {
+ url = defaultPathSerializer({ path, url });
+ }
+ let search = query ? querySerializer(query) : '';
+ if (search.startsWith('?')) {
+ search = search.substring(1);
+ }
+ if (search) {
+ url += `?${search}`;
+ }
+ return url;
+};
+
+export function getValidRequestBody(options: {
+ body?: unknown;
+ bodySerializer?: BodySerializer | null;
+ serializedBody?: unknown;
+}) {
+ const hasBody = options.body !== undefined;
+ const isSerializedBody = hasBody && options.bodySerializer;
+
+ if (isSerializedBody) {
+ if ('serializedBody' in options) {
+ const hasSerializedBody =
+ options.serializedBody !== undefined && options.serializedBody !== '';
+
+ return hasSerializedBody ? options.serializedBody : null;
+ }
+
+ // not all clients implement a serializedBody property (i.e. client-axios)
+ return options.body !== '' ? options.body : null;
+ }
+
+ // plain/text body
+ if (hasBody) {
+ return options.body;
+ }
+
+ // no body was provided
+ return undefined;
+}
diff --git a/examples/openapi-ts-swr/src/client/index.ts b/examples/openapi-ts-swr/src/client/index.ts
new file mode 100644
index 0000000000..57ed02bf5f
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/index.ts
@@ -0,0 +1,4 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+export * from './sdk.gen';
+export type * from './types.gen';
diff --git a/examples/openapi-ts-swr/src/client/schemas.gen.ts b/examples/openapi-ts-swr/src/client/schemas.gen.ts
new file mode 100644
index 0000000000..646632e830
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/schemas.gen.ts
@@ -0,0 +1,188 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+export const OrderSchema = {
+ properties: {
+ complete: {
+ type: 'boolean',
+ },
+ id: {
+ example: 10,
+ format: 'int64',
+ type: 'integer',
+ },
+ petId: {
+ example: 198772,
+ format: 'int64',
+ type: 'integer',
+ },
+ quantity: {
+ example: 7,
+ format: 'int32',
+ type: 'integer',
+ },
+ shipDate: {
+ format: 'date-time',
+ type: 'string',
+ },
+ status: {
+ description: 'Order Status',
+ enum: ['placed', 'approved', 'delivered'],
+ example: 'approved',
+ type: 'string',
+ },
+ },
+ type: 'object',
+ 'x-swagger-router-model': 'io.swagger.petstore.model.Order',
+ xml: {
+ name: 'order',
+ },
+} as const;
+
+export const CategorySchema = {
+ properties: {
+ id: {
+ example: 1,
+ format: 'int64',
+ type: 'integer',
+ },
+ name: {
+ example: 'Dogs',
+ type: 'string',
+ },
+ },
+ type: 'object',
+ 'x-swagger-router-model': 'io.swagger.petstore.model.Category',
+ xml: {
+ name: 'category',
+ },
+} as const;
+
+export const UserSchema = {
+ properties: {
+ email: {
+ example: 'john@email.com',
+ type: 'string',
+ },
+ firstName: {
+ example: 'John',
+ type: 'string',
+ },
+ id: {
+ example: 10,
+ format: 'int64',
+ type: 'integer',
+ },
+ lastName: {
+ example: 'James',
+ type: 'string',
+ },
+ password: {
+ example: '12345',
+ type: 'string',
+ },
+ phone: {
+ example: '12345',
+ type: 'string',
+ },
+ userStatus: {
+ description: 'User Status',
+ example: 1,
+ format: 'int32',
+ type: 'integer',
+ },
+ username: {
+ example: 'theUser',
+ type: 'string',
+ },
+ },
+ type: 'object',
+ 'x-swagger-router-model': 'io.swagger.petstore.model.User',
+ xml: {
+ name: 'user',
+ },
+} as const;
+
+export const TagSchema = {
+ properties: {
+ id: {
+ format: 'int64',
+ type: 'integer',
+ },
+ name: {
+ type: 'string',
+ },
+ },
+ type: 'object',
+ 'x-swagger-router-model': 'io.swagger.petstore.model.Tag',
+ xml: {
+ name: 'tag',
+ },
+} as const;
+
+export const PetSchema = {
+ properties: {
+ category: {
+ $ref: '#/components/schemas/Category',
+ },
+ id: {
+ example: 10,
+ format: 'int64',
+ type: 'integer',
+ },
+ name: {
+ example: 'doggie',
+ type: 'string',
+ },
+ photoUrls: {
+ items: {
+ type: 'string',
+ xml: {
+ name: 'photoUrl',
+ },
+ },
+ type: 'array',
+ xml: {
+ wrapped: true,
+ },
+ },
+ status: {
+ description: 'pet status in the store',
+ enum: ['available', 'pending', 'sold'],
+ type: 'string',
+ },
+ tags: {
+ items: {
+ $ref: '#/components/schemas/Tag',
+ },
+ type: 'array',
+ xml: {
+ wrapped: true,
+ },
+ },
+ },
+ required: ['name', 'photoUrls'],
+ type: 'object',
+ 'x-swagger-router-model': 'io.swagger.petstore.model.Pet',
+ xml: {
+ name: 'pet',
+ },
+} as const;
+
+export const ApiResponseSchema = {
+ properties: {
+ code: {
+ format: 'int32',
+ type: 'integer',
+ },
+ message: {
+ type: 'string',
+ },
+ type: {
+ type: 'string',
+ },
+ },
+ type: 'object',
+ xml: {
+ name: '##default',
+ },
+} as const;
diff --git a/examples/openapi-ts-swr/src/client/sdk.gen.ts b/examples/openapi-ts-swr/src/client/sdk.gen.ts
new file mode 100644
index 0000000000..f424fe675e
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/sdk.gen.ts
@@ -0,0 +1,486 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type { Client, Options as Options2, TDataShape } from './client';
+import { client } from './client.gen';
+import type {
+ AddPetData,
+ AddPetErrors,
+ AddPetResponses,
+ CreateUserData,
+ CreateUserErrors,
+ CreateUserResponses,
+ CreateUsersWithListInputData,
+ CreateUsersWithListInputErrors,
+ CreateUsersWithListInputResponses,
+ DeleteOrderData,
+ DeleteOrderErrors,
+ DeleteOrderResponses,
+ DeletePetData,
+ DeletePetErrors,
+ DeletePetResponses,
+ DeleteUserData,
+ DeleteUserErrors,
+ DeleteUserResponses,
+ FindPetsByStatusData,
+ FindPetsByStatusErrors,
+ FindPetsByStatusResponses,
+ FindPetsByTagsData,
+ FindPetsByTagsErrors,
+ FindPetsByTagsResponses,
+ GetInventoryData,
+ GetInventoryErrors,
+ GetInventoryResponses,
+ GetOrderByIdData,
+ GetOrderByIdErrors,
+ GetOrderByIdResponses,
+ GetPetByIdData,
+ GetPetByIdErrors,
+ GetPetByIdResponses,
+ GetUserByNameData,
+ GetUserByNameErrors,
+ GetUserByNameResponses,
+ LoginUserData,
+ LoginUserErrors,
+ LoginUserResponses,
+ LogoutUserData,
+ LogoutUserErrors,
+ LogoutUserResponses,
+ PlaceOrderData,
+ PlaceOrderErrors,
+ PlaceOrderResponses,
+ UpdatePetData,
+ UpdatePetErrors,
+ UpdatePetResponses,
+ UpdatePetWithFormData,
+ UpdatePetWithFormErrors,
+ UpdatePetWithFormResponses,
+ UpdateUserData,
+ UpdateUserErrors,
+ UpdateUserResponses,
+ UploadFileData,
+ UploadFileErrors,
+ UploadFileResponses,
+} from './types.gen';
+
+export type Options<
+ TData extends TDataShape = TDataShape,
+ ThrowOnError extends boolean = boolean,
+> = Options2 & {
+ /**
+ * You can provide a client instance returned by `createClient()` instead of
+ * individual options. This might be also useful if you want to implement a
+ * custom client.
+ */
+ client?: Client;
+ /**
+ * You can pass arbitrary values through the `meta` object. This can be
+ * used to access values that aren't defined as part of the SDK function.
+ */
+ meta?: Record;
+};
+
+/**
+ * Add a new pet to the store.
+ *
+ * Add a new pet to the store.
+ */
+export const addPet = (
+ options: Options,
+) =>
+ (options.client ?? client).post({
+ security: [
+ {
+ scheme: 'bearer',
+ type: 'http',
+ },
+ ],
+ url: '/pet',
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options.headers,
+ },
+ });
+
+/**
+ * Update an existing pet.
+ *
+ * Update an existing pet by Id.
+ */
+export const updatePet = (
+ options: Options,
+) =>
+ (options.client ?? client).put<
+ UpdatePetResponses,
+ UpdatePetErrors,
+ ThrowOnError
+ >({
+ security: [
+ {
+ scheme: 'bearer',
+ type: 'http',
+ },
+ ],
+ url: '/pet',
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options.headers,
+ },
+ });
+
+/**
+ * Finds Pets by status.
+ *
+ * Multiple status values can be provided with comma separated strings.
+ */
+export const findPetsByStatus = (
+ options: Options,
+) =>
+ (options.client ?? client).get<
+ FindPetsByStatusResponses,
+ FindPetsByStatusErrors,
+ ThrowOnError
+ >({
+ security: [
+ {
+ scheme: 'bearer',
+ type: 'http',
+ },
+ ],
+ url: '/pet/findByStatus',
+ ...options,
+ });
+
+/**
+ * Finds Pets by tags.
+ *
+ * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
+ */
+export const findPetsByTags = (
+ options: Options,
+) =>
+ (options.client ?? client).get<
+ FindPetsByTagsResponses,
+ FindPetsByTagsErrors,
+ ThrowOnError
+ >({
+ security: [
+ {
+ scheme: 'bearer',
+ type: 'http',
+ },
+ ],
+ url: '/pet/findByTags',
+ ...options,
+ });
+
+/**
+ * Deletes a pet.
+ *
+ * Delete a pet.
+ */
+export const deletePet = (
+ options: Options,
+) =>
+ (options.client ?? client).delete<
+ DeletePetResponses,
+ DeletePetErrors,
+ ThrowOnError
+ >({
+ security: [
+ {
+ scheme: 'bearer',
+ type: 'http',
+ },
+ ],
+ url: '/pet/{petId}',
+ ...options,
+ });
+
+/**
+ * Find pet by ID.
+ *
+ * Returns a single pet.
+ */
+export const getPetById = (
+ options: Options,
+) =>
+ (options.client ?? client).get<
+ GetPetByIdResponses,
+ GetPetByIdErrors,
+ ThrowOnError
+ >({
+ security: [
+ {
+ name: 'api_key',
+ type: 'apiKey',
+ },
+ {
+ scheme: 'bearer',
+ type: 'http',
+ },
+ ],
+ url: '/pet/{petId}',
+ ...options,
+ });
+
+/**
+ * Updates a pet in the store with form data.
+ *
+ * Updates a pet resource based on the form data.
+ */
+export const updatePetWithForm = (
+ options: Options,
+) =>
+ (options.client ?? client).post<
+ UpdatePetWithFormResponses,
+ UpdatePetWithFormErrors,
+ ThrowOnError
+ >({
+ security: [
+ {
+ scheme: 'bearer',
+ type: 'http',
+ },
+ ],
+ url: '/pet/{petId}',
+ ...options,
+ });
+
+/**
+ * Uploads an image.
+ *
+ * Upload image of the pet.
+ */
+export const uploadFile = (
+ options: Options,
+) =>
+ (options.client ?? client).post<
+ UploadFileResponses,
+ UploadFileErrors,
+ ThrowOnError
+ >({
+ bodySerializer: null,
+ security: [
+ {
+ scheme: 'bearer',
+ type: 'http',
+ },
+ ],
+ url: '/pet/{petId}/uploadImage',
+ ...options,
+ headers: {
+ 'Content-Type': 'application/octet-stream',
+ ...options.headers,
+ },
+ });
+
+/**
+ * Returns pet inventories by status.
+ *
+ * Returns a map of status codes to quantities.
+ */
+export const getInventory = (
+ options?: Options,
+) =>
+ (options?.client ?? client).get<
+ GetInventoryResponses,
+ GetInventoryErrors,
+ ThrowOnError
+ >({
+ security: [
+ {
+ name: 'api_key',
+ type: 'apiKey',
+ },
+ ],
+ url: '/store/inventory',
+ ...options,
+ });
+
+/**
+ * Place an order for a pet.
+ *
+ * Place a new order in the store.
+ */
+export const placeOrder = (
+ options?: Options,
+) =>
+ (options?.client ?? client).post<
+ PlaceOrderResponses,
+ PlaceOrderErrors,
+ ThrowOnError
+ >({
+ url: '/store/order',
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options?.headers,
+ },
+ });
+
+/**
+ * Delete purchase order by identifier.
+ *
+ * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors.
+ */
+export const deleteOrder = (
+ options: Options,
+) =>
+ (options.client ?? client).delete<
+ DeleteOrderResponses,
+ DeleteOrderErrors,
+ ThrowOnError
+ >({
+ url: '/store/order/{orderId}',
+ ...options,
+ });
+
+/**
+ * Find purchase order by ID.
+ *
+ * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.
+ */
+export const getOrderById = (
+ options: Options,
+) =>
+ (options.client ?? client).get<
+ GetOrderByIdResponses,
+ GetOrderByIdErrors,
+ ThrowOnError
+ >({
+ url: '/store/order/{orderId}',
+ ...options,
+ });
+
+/**
+ * Create user.
+ *
+ * This can only be done by the logged in user.
+ */
+export const createUser = (
+ options?: Options,
+) =>
+ (options?.client ?? client).post<
+ CreateUserResponses,
+ CreateUserErrors,
+ ThrowOnError
+ >({
+ url: '/user',
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options?.headers,
+ },
+ });
+
+/**
+ * Creates list of users with given input array.
+ *
+ * Creates list of users with given input array.
+ */
+export const createUsersWithListInput = (
+ options?: Options,
+) =>
+ (options?.client ?? client).post<
+ CreateUsersWithListInputResponses,
+ CreateUsersWithListInputErrors,
+ ThrowOnError
+ >({
+ url: '/user/createWithList',
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options?.headers,
+ },
+ });
+
+/**
+ * Logs user into the system.
+ *
+ * Log into the system.
+ */
+export const loginUser = (
+ options?: Options,
+) =>
+ (options?.client ?? client).get<
+ LoginUserResponses,
+ LoginUserErrors,
+ ThrowOnError
+ >({
+ url: '/user/login',
+ ...options,
+ });
+
+/**
+ * Logs out current logged in user session.
+ *
+ * Log user out of the system.
+ */
+export const logoutUser = (
+ options?: Options,
+) =>
+ (options?.client ?? client).get<
+ LogoutUserResponses,
+ LogoutUserErrors,
+ ThrowOnError
+ >({
+ url: '/user/logout',
+ ...options,
+ });
+
+/**
+ * Delete user resource.
+ *
+ * This can only be done by the logged in user.
+ */
+export const deleteUser = (
+ options: Options,
+) =>
+ (options.client ?? client).delete<
+ DeleteUserResponses,
+ DeleteUserErrors,
+ ThrowOnError
+ >({
+ url: '/user/{username}',
+ ...options,
+ });
+
+/**
+ * Get user by user name.
+ *
+ * Get user detail based on username.
+ */
+export const getUserByName = (
+ options: Options,
+) =>
+ (options.client ?? client).get<
+ GetUserByNameResponses,
+ GetUserByNameErrors,
+ ThrowOnError
+ >({
+ url: '/user/{username}',
+ ...options,
+ });
+
+/**
+ * Update user resource.
+ *
+ * This can only be done by the logged in user.
+ */
+export const updateUser = (
+ options: Options,
+) =>
+ (options.client ?? client).put<
+ UpdateUserResponses,
+ UpdateUserErrors,
+ ThrowOnError
+ >({
+ url: '/user/{username}',
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options.headers,
+ },
+ });
diff --git a/examples/openapi-ts-swr/src/client/swr.gen.ts b/examples/openapi-ts-swr/src/client/swr.gen.ts
new file mode 100644
index 0000000000..29ce197a74
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/swr.gen.ts
@@ -0,0 +1,459 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import {
+ addPet,
+ createUser,
+ createUsersWithListInput,
+ deleteOrder,
+ deletePet,
+ deleteUser,
+ findPetsByStatus,
+ findPetsByTags,
+ getInventory,
+ getOrderById,
+ getPetById,
+ getUserByName,
+ loginUser,
+ logoutUser,
+ type Options,
+ placeOrder,
+ updatePet,
+ updatePetWithForm,
+ updateUser,
+ uploadFile,
+} from './sdk.gen';
+import type {
+ AddPetData,
+ CreateUserData,
+ CreateUsersWithListInputData,
+ DeleteOrderData,
+ DeletePetData,
+ DeleteUserData,
+ FindPetsByStatusData,
+ FindPetsByTagsData,
+ GetInventoryData,
+ GetOrderByIdData,
+ GetPetByIdData,
+ GetUserByNameData,
+ LoginUserData,
+ LogoutUserData,
+ PlaceOrderData,
+ UpdatePetData,
+ UpdatePetWithFormData,
+ UpdateUserData,
+ UploadFileData,
+} from './types.gen';
+
+/**
+ * Add a new pet to the store.
+ *
+ * Add a new pet to the store.
+ */
+export const addPetMutation = (options?: Options) => ({
+ fetcher: async (
+ _key: string[],
+ {
+ arg,
+ }: {
+ arg: Options;
+ },
+ ) => {
+ const { data } = await addPet({
+ ...options,
+ ...arg,
+ throwOnError: true,
+ });
+ return data;
+ },
+ key: ['/pet'],
+});
+
+/**
+ * Update an existing pet.
+ *
+ * Update an existing pet by Id.
+ */
+export const updatePetMutation = (options?: Options) => ({
+ fetcher: async (
+ _key: string[],
+ {
+ arg,
+ }: {
+ arg: Options;
+ },
+ ) => {
+ const { data } = await updatePet({
+ ...options,
+ ...arg,
+ throwOnError: true,
+ });
+ return data;
+ },
+ key: ['/pet'],
+});
+
+export const findPetsByStatusKey = (options: Options) => [
+ '/pet/findByStatus',
+ options,
+];
+
+/**
+ * Finds Pets by status.
+ *
+ * Multiple status values can be provided with comma separated strings.
+ */
+export const findPetsByStatusOptions = (
+ options: Options,
+) => ({
+ fetcher: async () => {
+ const { data } = await findPetsByStatus({ ...options, throwOnError: true });
+ return data;
+ },
+ key: findPetsByStatusKey(options),
+});
+
+export const findPetsByTagsKey = (options: Options) => [
+ '/pet/findByTags',
+ options,
+];
+
+/**
+ * Finds Pets by tags.
+ *
+ * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
+ */
+export const findPetsByTagsOptions = (
+ options: Options,
+) => ({
+ fetcher: async () => {
+ const { data } = await findPetsByTags({ ...options, throwOnError: true });
+ return data;
+ },
+ key: findPetsByTagsKey(options),
+});
+
+/**
+ * Deletes a pet.
+ *
+ * Delete a pet.
+ */
+export const deletePetMutation = (options?: Options) => ({
+ fetcher: async (
+ _key: string[] | [string, Options],
+ {
+ arg,
+ }: {
+ arg: Options;
+ },
+ ) => {
+ const { data } = await deletePet({
+ ...options,
+ ...arg,
+ throwOnError: true,
+ });
+ return data;
+ },
+ key: options?.path ? ['/pet/{petId}', options] : ['/pet/{petId}'],
+});
+
+export const getPetByIdKey = (options: Options) => [
+ '/pet/{petId}',
+ options,
+];
+
+/**
+ * Find pet by ID.
+ *
+ * Returns a single pet.
+ */
+export const getPetByIdOptions = (options: Options) => ({
+ fetcher: async () => {
+ const { data } = await getPetById({ ...options, throwOnError: true });
+ return data;
+ },
+ key: getPetByIdKey(options),
+});
+
+/**
+ * Updates a pet in the store with form data.
+ *
+ * Updates a pet resource based on the form data.
+ */
+export const updatePetWithFormMutation = (
+ options?: Options,
+) => ({
+ fetcher: async (
+ _key: string[] | [string, Options],
+ {
+ arg,
+ }: {
+ arg: Options;
+ },
+ ) => {
+ const { data } = await updatePetWithForm({
+ ...options,
+ ...arg,
+ throwOnError: true,
+ });
+ return data;
+ },
+ key: options?.path ? ['/pet/{petId}', options] : ['/pet/{petId}'],
+});
+
+/**
+ * Uploads an image.
+ *
+ * Upload image of the pet.
+ */
+export const uploadFileMutation = (options?: Options) => ({
+ fetcher: async (
+ _key: string[] | [string, Options],
+ {
+ arg,
+ }: {
+ arg: Options;
+ },
+ ) => {
+ const { data } = await uploadFile({
+ ...options,
+ ...arg,
+ throwOnError: true,
+ });
+ return data;
+ },
+ key: options?.path
+ ? ['/pet/{petId}/uploadImage', options]
+ : ['/pet/{petId}/uploadImage'],
+});
+
+export const getInventoryKey = () => ['/store/inventory'];
+
+/**
+ * Returns pet inventories by status.
+ *
+ * Returns a map of status codes to quantities.
+ */
+export const getInventoryOptions = (options?: Options) => ({
+ fetcher: async () => {
+ const { data } = await getInventory({ ...options, throwOnError: true });
+ return data;
+ },
+ key: getInventoryKey(),
+});
+
+/**
+ * Place an order for a pet.
+ *
+ * Place a new order in the store.
+ */
+export const placeOrderMutation = (options?: Options) => ({
+ fetcher: async (
+ _key: string[],
+ {
+ arg,
+ }: {
+ arg: Options;
+ },
+ ) => {
+ const { data } = await placeOrder({
+ ...options,
+ ...arg,
+ throwOnError: true,
+ });
+ return data;
+ },
+ key: ['/store/order'],
+});
+
+/**
+ * Delete purchase order by identifier.
+ *
+ * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors.
+ */
+export const deleteOrderMutation = (options?: Options) => ({
+ fetcher: async (
+ _key: string[] | [string, Options],
+ {
+ arg,
+ }: {
+ arg: Options;
+ },
+ ) => {
+ const { data } = await deleteOrder({
+ ...options,
+ ...arg,
+ throwOnError: true,
+ });
+ return data;
+ },
+ key: options?.path
+ ? ['/store/order/{orderId}', options]
+ : ['/store/order/{orderId}'],
+});
+
+export const getOrderByIdKey = (options: Options) => [
+ '/store/order/{orderId}',
+ options,
+];
+
+/**
+ * Find purchase order by ID.
+ *
+ * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.
+ */
+export const getOrderByIdOptions = (options: Options) => ({
+ fetcher: async () => {
+ const { data } = await getOrderById({ ...options, throwOnError: true });
+ return data;
+ },
+ key: getOrderByIdKey(options),
+});
+
+/**
+ * Create user.
+ *
+ * This can only be done by the logged in user.
+ */
+export const createUserMutation = (options?: Options) => ({
+ fetcher: async (
+ _key: string[],
+ {
+ arg,
+ }: {
+ arg: Options;
+ },
+ ) => {
+ const { data } = await createUser({
+ ...options,
+ ...arg,
+ throwOnError: true,
+ });
+ return data;
+ },
+ key: ['/user'],
+});
+
+/**
+ * Creates list of users with given input array.
+ *
+ * Creates list of users with given input array.
+ */
+export const createUsersWithListInputMutation = (
+ options?: Options,
+) => ({
+ fetcher: async (
+ _key: string[],
+ {
+ arg,
+ }: {
+ arg: Options;
+ },
+ ) => {
+ const { data } = await createUsersWithListInput({
+ ...options,
+ ...arg,
+ throwOnError: true,
+ });
+ return data;
+ },
+ key: ['/user/createWithList'],
+});
+
+export const loginUserKey = (options?: Options) => [
+ '/user/login',
+ options,
+];
+
+/**
+ * Logs user into the system.
+ *
+ * Log into the system.
+ */
+export const loginUserOptions = (options?: Options) => ({
+ fetcher: async () => {
+ const { data } = await loginUser({ ...options, throwOnError: true });
+ return data;
+ },
+ key: loginUserKey(options),
+});
+
+export const logoutUserKey = () => ['/user/logout'];
+
+/**
+ * Logs out current logged in user session.
+ *
+ * Log user out of the system.
+ */
+export const logoutUserOptions = (options?: Options) => ({
+ fetcher: async () => {
+ const { data } = await logoutUser({ ...options, throwOnError: true });
+ return data;
+ },
+ key: logoutUserKey(),
+});
+
+/**
+ * Delete user resource.
+ *
+ * This can only be done by the logged in user.
+ */
+export const deleteUserMutation = (options?: Options) => ({
+ fetcher: async (
+ _key: string[] | [string, Options],
+ {
+ arg,
+ }: {
+ arg: Options;
+ },
+ ) => {
+ const { data } = await deleteUser({
+ ...options,
+ ...arg,
+ throwOnError: true,
+ });
+ return data;
+ },
+ key: options?.path ? ['/user/{username}', options] : ['/user/{username}'],
+});
+
+export const getUserByNameKey = (options: Options) => [
+ '/user/{username}',
+ options,
+];
+
+/**
+ * Get user by user name.
+ *
+ * Get user detail based on username.
+ */
+export const getUserByNameOptions = (options: Options) => ({
+ fetcher: async () => {
+ const { data } = await getUserByName({ ...options, throwOnError: true });
+ return data;
+ },
+ key: getUserByNameKey(options),
+});
+
+/**
+ * Update user resource.
+ *
+ * This can only be done by the logged in user.
+ */
+export const updateUserMutation = (options?: Options) => ({
+ fetcher: async (
+ _key: string[] | [string, Options],
+ {
+ arg,
+ }: {
+ arg: Options;
+ },
+ ) => {
+ const { data } = await updateUser({
+ ...options,
+ ...arg,
+ throwOnError: true,
+ });
+ return data;
+ },
+ key: options?.path ? ['/user/{username}', options] : ['/user/{username}'],
+});
diff --git a/examples/openapi-ts-swr/src/client/types.gen.ts b/examples/openapi-ts-swr/src/client/types.gen.ts
new file mode 100644
index 0000000000..a2e6be0fa4
--- /dev/null
+++ b/examples/openapi-ts-swr/src/client/types.gen.ts
@@ -0,0 +1,699 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+export type ClientOptions = {
+ baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {});
+};
+
+export type Order = {
+ complete?: boolean;
+ id?: number;
+ petId?: number;
+ quantity?: number;
+ shipDate?: string;
+ /**
+ * Order Status
+ */
+ status?: 'placed' | 'approved' | 'delivered';
+};
+
+export type Category = {
+ id?: number;
+ name?: string;
+};
+
+export type User = {
+ email?: string;
+ firstName?: string;
+ id?: number;
+ lastName?: string;
+ password?: string;
+ phone?: string;
+ /**
+ * User Status
+ */
+ userStatus?: number;
+ username?: string;
+};
+
+export type Tag = {
+ id?: number;
+ name?: string;
+};
+
+export type Pet = {
+ category?: Category;
+ id?: number;
+ name: string;
+ photoUrls: Array;
+ /**
+ * pet status in the store
+ */
+ status?: 'available' | 'pending' | 'sold';
+ tags?: Array;
+};
+
+export type ApiResponse = {
+ code?: number;
+ message?: string;
+ type?: string;
+};
+
+export type Pet2 = Pet;
+
+/**
+ * List of user object
+ */
+export type UserArray = Array;
+
+export type AddPetData = {
+ /**
+ * Create a new pet in the store
+ */
+ body: Pet;
+ path?: never;
+ query?: never;
+ url: '/pet';
+};
+
+export type AddPetErrors = {
+ /**
+ * Invalid input
+ */
+ 400: unknown;
+ /**
+ * Validation exception
+ */
+ 422: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type AddPetResponses = {
+ /**
+ * Successful operation
+ */
+ 200: Pet;
+};
+
+export type AddPetResponse = AddPetResponses[keyof AddPetResponses];
+
+export type UpdatePetData = {
+ /**
+ * Update an existent pet in the store
+ */
+ body: Pet;
+ path?: never;
+ query?: never;
+ url: '/pet';
+};
+
+export type UpdatePetErrors = {
+ /**
+ * Invalid ID supplied
+ */
+ 400: unknown;
+ /**
+ * Pet not found
+ */
+ 404: unknown;
+ /**
+ * Validation exception
+ */
+ 422: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type UpdatePetResponses = {
+ /**
+ * Successful operation
+ */
+ 200: Pet;
+};
+
+export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses];
+
+export type FindPetsByStatusData = {
+ body?: never;
+ path?: never;
+ query: {
+ /**
+ * Status values that need to be considered for filter
+ */
+ status: 'available' | 'pending' | 'sold';
+ };
+ url: '/pet/findByStatus';
+};
+
+export type FindPetsByStatusErrors = {
+ /**
+ * Invalid status value
+ */
+ 400: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type FindPetsByStatusResponses = {
+ /**
+ * successful operation
+ */
+ 200: Array;
+};
+
+export type FindPetsByStatusResponse =
+ FindPetsByStatusResponses[keyof FindPetsByStatusResponses];
+
+export type FindPetsByTagsData = {
+ body?: never;
+ path?: never;
+ query: {
+ /**
+ * Tags to filter by
+ */
+ tags: Array;
+ };
+ url: '/pet/findByTags';
+};
+
+export type FindPetsByTagsErrors = {
+ /**
+ * Invalid tag value
+ */
+ 400: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type FindPetsByTagsResponses = {
+ /**
+ * successful operation
+ */
+ 200: Array;
+};
+
+export type FindPetsByTagsResponse =
+ FindPetsByTagsResponses[keyof FindPetsByTagsResponses];
+
+export type DeletePetData = {
+ body?: never;
+ headers?: {
+ api_key?: string;
+ };
+ path: {
+ /**
+ * Pet id to delete
+ */
+ petId: number;
+ };
+ query?: never;
+ url: '/pet/{petId}';
+};
+
+export type DeletePetErrors = {
+ /**
+ * Invalid pet value
+ */
+ 400: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type DeletePetResponses = {
+ /**
+ * Pet deleted
+ */
+ 200: unknown;
+};
+
+export type GetPetByIdData = {
+ body?: never;
+ path: {
+ /**
+ * ID of pet to return
+ */
+ petId: number;
+ };
+ query?: never;
+ url: '/pet/{petId}';
+};
+
+export type GetPetByIdErrors = {
+ /**
+ * Invalid ID supplied
+ */
+ 400: unknown;
+ /**
+ * Pet not found
+ */
+ 404: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type GetPetByIdResponses = {
+ /**
+ * successful operation
+ */
+ 200: Pet;
+};
+
+export type GetPetByIdResponse = GetPetByIdResponses[keyof GetPetByIdResponses];
+
+export type UpdatePetWithFormData = {
+ body?: never;
+ path: {
+ /**
+ * ID of pet that needs to be updated
+ */
+ petId: number;
+ };
+ query?: {
+ /**
+ * Name of pet that needs to be updated
+ */
+ name?: string;
+ /**
+ * Status of pet that needs to be updated
+ */
+ status?: string;
+ };
+ url: '/pet/{petId}';
+};
+
+export type UpdatePetWithFormErrors = {
+ /**
+ * Invalid input
+ */
+ 400: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type UpdatePetWithFormResponses = {
+ /**
+ * successful operation
+ */
+ 200: Pet;
+};
+
+export type UpdatePetWithFormResponse =
+ UpdatePetWithFormResponses[keyof UpdatePetWithFormResponses];
+
+export type UploadFileData = {
+ body?: Blob | File;
+ path: {
+ /**
+ * ID of pet to update
+ */
+ petId: number;
+ };
+ query?: {
+ /**
+ * Additional Metadata
+ */
+ additionalMetadata?: string;
+ };
+ url: '/pet/{petId}/uploadImage';
+};
+
+export type UploadFileErrors = {
+ /**
+ * No file uploaded
+ */
+ 400: unknown;
+ /**
+ * Pet not found
+ */
+ 404: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type UploadFileResponses = {
+ /**
+ * successful operation
+ */
+ 200: ApiResponse;
+};
+
+export type UploadFileResponse = UploadFileResponses[keyof UploadFileResponses];
+
+export type GetInventoryData = {
+ body?: never;
+ path?: never;
+ query?: never;
+ url: '/store/inventory';
+};
+
+export type GetInventoryErrors = {
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type GetInventoryResponses = {
+ /**
+ * successful operation
+ */
+ 200: {
+ [key: string]: number;
+ };
+};
+
+export type GetInventoryResponse =
+ GetInventoryResponses[keyof GetInventoryResponses];
+
+export type PlaceOrderData = {
+ body?: Order;
+ path?: never;
+ query?: never;
+ url: '/store/order';
+};
+
+export type PlaceOrderErrors = {
+ /**
+ * Invalid input
+ */
+ 400: unknown;
+ /**
+ * Validation exception
+ */
+ 422: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type PlaceOrderResponses = {
+ /**
+ * successful operation
+ */
+ 200: Order;
+};
+
+export type PlaceOrderResponse = PlaceOrderResponses[keyof PlaceOrderResponses];
+
+export type DeleteOrderData = {
+ body?: never;
+ path: {
+ /**
+ * ID of the order that needs to be deleted
+ */
+ orderId: number;
+ };
+ query?: never;
+ url: '/store/order/{orderId}';
+};
+
+export type DeleteOrderErrors = {
+ /**
+ * Invalid ID supplied
+ */
+ 400: unknown;
+ /**
+ * Order not found
+ */
+ 404: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type DeleteOrderResponses = {
+ /**
+ * order deleted
+ */
+ 200: unknown;
+};
+
+export type GetOrderByIdData = {
+ body?: never;
+ path: {
+ /**
+ * ID of order that needs to be fetched
+ */
+ orderId: number;
+ };
+ query?: never;
+ url: '/store/order/{orderId}';
+};
+
+export type GetOrderByIdErrors = {
+ /**
+ * Invalid ID supplied
+ */
+ 400: unknown;
+ /**
+ * Order not found
+ */
+ 404: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type GetOrderByIdResponses = {
+ /**
+ * successful operation
+ */
+ 200: Order;
+};
+
+export type GetOrderByIdResponse =
+ GetOrderByIdResponses[keyof GetOrderByIdResponses];
+
+export type CreateUserData = {
+ /**
+ * Created user object
+ */
+ body?: User;
+ path?: never;
+ query?: never;
+ url: '/user';
+};
+
+export type CreateUserErrors = {
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type CreateUserResponses = {
+ /**
+ * successful operation
+ */
+ 200: User;
+};
+
+export type CreateUserResponse = CreateUserResponses[keyof CreateUserResponses];
+
+export type CreateUsersWithListInputData = {
+ body?: Array;
+ path?: never;
+ query?: never;
+ url: '/user/createWithList';
+};
+
+export type CreateUsersWithListInputErrors = {
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type CreateUsersWithListInputResponses = {
+ /**
+ * Successful operation
+ */
+ 200: User;
+};
+
+export type CreateUsersWithListInputResponse =
+ CreateUsersWithListInputResponses[keyof CreateUsersWithListInputResponses];
+
+export type LoginUserData = {
+ body?: never;
+ path?: never;
+ query?: {
+ /**
+ * The password for login in clear text
+ */
+ password?: string;
+ /**
+ * The user name for login
+ */
+ username?: string;
+ };
+ url: '/user/login';
+};
+
+export type LoginUserErrors = {
+ /**
+ * Invalid username/password supplied
+ */
+ 400: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type LoginUserResponses = {
+ /**
+ * successful operation
+ */
+ 200: string;
+};
+
+export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses];
+
+export type LogoutUserData = {
+ body?: never;
+ path?: never;
+ query?: never;
+ url: '/user/logout';
+};
+
+export type LogoutUserErrors = {
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type LogoutUserResponses = {
+ /**
+ * successful operation
+ */
+ 200: unknown;
+};
+
+export type DeleteUserData = {
+ body?: never;
+ path: {
+ /**
+ * The name that needs to be deleted
+ */
+ username: string;
+ };
+ query?: never;
+ url: '/user/{username}';
+};
+
+export type DeleteUserErrors = {
+ /**
+ * Invalid username supplied
+ */
+ 400: unknown;
+ /**
+ * User not found
+ */
+ 404: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type DeleteUserResponses = {
+ /**
+ * User deleted
+ */
+ 200: unknown;
+};
+
+export type GetUserByNameData = {
+ body?: never;
+ path: {
+ /**
+ * The name that needs to be fetched. Use user1 for testing
+ */
+ username: string;
+ };
+ query?: never;
+ url: '/user/{username}';
+};
+
+export type GetUserByNameErrors = {
+ /**
+ * Invalid username supplied
+ */
+ 400: unknown;
+ /**
+ * User not found
+ */
+ 404: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type GetUserByNameResponses = {
+ /**
+ * successful operation
+ */
+ 200: User;
+};
+
+export type GetUserByNameResponse =
+ GetUserByNameResponses[keyof GetUserByNameResponses];
+
+export type UpdateUserData = {
+ /**
+ * Update an existent user in the store
+ */
+ body?: User;
+ path: {
+ /**
+ * name that need to be deleted
+ */
+ username: string;
+ };
+ query?: never;
+ url: '/user/{username}';
+};
+
+export type UpdateUserErrors = {
+ /**
+ * bad request
+ */
+ 400: unknown;
+ /**
+ * user not found
+ */
+ 404: unknown;
+ /**
+ * Unexpected error
+ */
+ default: unknown;
+};
+
+export type UpdateUserResponses = {
+ /**
+ * successful operation
+ */
+ 200: unknown;
+};
diff --git a/examples/openapi-ts-swr/src/main.tsx b/examples/openapi-ts-swr/src/main.tsx
new file mode 100644
index 0000000000..8c2717dabb
--- /dev/null
+++ b/examples/openapi-ts-swr/src/main.tsx
@@ -0,0 +1,35 @@
+import '@radix-ui/themes/styles.css';
+
+import { Theme } from '@radix-ui/themes';
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { SWRConfig } from 'swr';
+
+import App from './App.tsx';
+import { client } from './client/client.gen';
+
+// configure internal service client
+client.setConfig({
+ // set default base url for requests
+ baseUrl: 'https://petstore3.swagger.io/api/v3',
+ // set default headers for requests
+ headers: {
+ Authorization: 'Bearer ',
+ },
+});
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+
+ ,
+);
diff --git a/examples/openapi-ts-swr/src/vite-env.d.ts b/examples/openapi-ts-swr/src/vite-env.d.ts
new file mode 100644
index 0000000000..11f02fe2a0
--- /dev/null
+++ b/examples/openapi-ts-swr/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/openapi-ts-swr/tailwind.config.js b/examples/openapi-ts-swr/tailwind.config.js
new file mode 100644
index 0000000000..0284c5614d
--- /dev/null
+++ b/examples/openapi-ts-swr/tailwind.config.js
@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./index.html', './src/**/*.{html,js,ts,jsx,tsx}'],
+ plugins: [],
+ theme: {
+ extend: {},
+ },
+};
diff --git a/examples/openapi-ts-swr/tsconfig.json b/examples/openapi-ts-swr/tsconfig.json
new file mode 100644
index 0000000000..04664de395
--- /dev/null
+++ b/examples/openapi-ts-swr/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/examples/openapi-ts-swr/tsconfig.node.json b/examples/openapi-ts-swr/tsconfig.node.json
new file mode 100644
index 0000000000..97ede7ee6f
--- /dev/null
+++ b/examples/openapi-ts-swr/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/examples/openapi-ts-swr/vite.config.ts b/examples/openapi-ts-swr/vite.config.ts
new file mode 100644
index 0000000000..8cbf22f1ba
--- /dev/null
+++ b/examples/openapi-ts-swr/vite.config.ts
@@ -0,0 +1,7 @@
+import { createViteConfig } from '@config/vite-base';
+import react from '@vitejs/plugin-react';
+
+// https://vitejs.dev/config/
+export default createViteConfig({
+ plugins: [react()],
+});
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts
index f81a9e7863..b0628d02ab 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts
@@ -76,6 +76,7 @@ export const createClient = (config: Config = {}): Client => {
try {
// assign Axios here for consistency with fetch
const _axios = opts.axios!;
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { auth, ...optsWithoutAuth } = opts;
const response = await _axios({
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/react-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/react-query/axios/client/client.gen.ts
index f81a9e7863..b0628d02ab 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/react-query/axios/client/client.gen.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/react-query/axios/client/client.gen.ts
@@ -76,6 +76,7 @@ export const createClient = (config: Config = {}): Client => {
try {
// assign Axios here for consistency with fetch
const _axios = opts.axios!;
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { auth, ...optsWithoutAuth } = opts;
const response = await _axios({
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts
index f81a9e7863..b0628d02ab 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts
@@ -76,6 +76,7 @@ export const createClient = (config: Config = {}): Client => {
try {
// assign Axios here for consistency with fetch
const _axios = opts.axios!;
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { auth, ...optsWithoutAuth } = opts;
const response = await _axios({
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts
index f81a9e7863..b0628d02ab 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts
@@ -76,6 +76,7 @@ export const createClient = (config: Config = {}): Client => {
try {
// assign Axios here for consistency with fetch
const _axios = opts.axios!;
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { auth, ...optsWithoutAuth } = opts;
const response = await _axios({
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts
index f81a9e7863..b0628d02ab 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts
@@ -76,6 +76,7 @@ export const createClient = (config: Config = {}): Client => {
try {
// assign Axios here for consistency with fetch
const _axios = opts.axios!;
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { auth, ...optsWithoutAuth } = opts;
const response = await _axios({
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/swr/asClass/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/swr/asClass/client.gen.ts
new file mode 100644
index 0000000000..cab3c70195
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/swr/asClass/client.gen.ts
@@ -0,0 +1,16 @@
+// 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 = (override?: Config) => Config & T>;
+
+export const client = createClient(createConfig());
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/swr/asClass/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/swr/asClass/client/client.gen.ts
new file mode 100644
index 0000000000..c2a5190c22
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/swr/asClass/client/client.gen.ts
@@ -0,0 +1,301 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import { createSseClient } from '../core/serverSentEvents.gen';
+import type { HttpMethod } from '../core/types.gen';
+import { getValidRequestBody } from '../core/utils.gen';
+import type {
+ Client,
+ Config,
+ RequestOptions,
+ ResolvedRequestOptions,
+} from './types.gen';
+import {
+ buildUrl,
+ createConfig,
+ createInterceptors,
+ getParseAs,
+ mergeConfigs,
+ mergeHeaders,
+ setAuthParams,
+} from './utils.gen';
+
+type ReqInit = Omit & {
+ body?: any;
+ headers: ReturnType;
+};
+
+export const createClient = (config: Config = {}): Client => {
+ let _config = mergeConfigs(createConfig(), config);
+
+ const getConfig = (): Config => ({ ..._config });
+
+ const setConfig = (config: Config): Config => {
+ _config = mergeConfigs(_config, config);
+ return getConfig();
+ };
+
+ const interceptors = createInterceptors<
+ Request,
+ Response,
+ unknown,
+ ResolvedRequestOptions
+ >();
+
+ const beforeRequest = async (options: RequestOptions) => {
+ const opts = {
+ ..._config,
+ ...options,
+ fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
+ headers: mergeHeaders(_config.headers, options.headers),
+ serializedBody: undefined,
+ };
+
+ if (opts.security) {
+ await setAuthParams({
+ ...opts,
+ security: opts.security,
+ });
+ }
+
+ if (opts.requestValidator) {
+ await opts.requestValidator(opts);
+ }
+
+ if (opts.body !== undefined && opts.bodySerializer) {
+ opts.serializedBody = opts.bodySerializer(opts.body);
+ }
+
+ // remove Content-Type header if body is empty to avoid sending invalid requests
+ if (opts.body === undefined || opts.serializedBody === '') {
+ opts.headers.delete('Content-Type');
+ }
+
+ const url = buildUrl(opts);
+
+ return { opts, url };
+ };
+
+ const request: Client['request'] = async (options) => {
+ // @ts-expect-error
+ const { opts, url } = await beforeRequest(options);
+ const requestInit: ReqInit = {
+ redirect: 'follow',
+ ...opts,
+ body: getValidRequestBody(opts),
+ };
+
+ let request = new Request(url, requestInit);
+
+ for (const fn of interceptors.request.fns) {
+ if (fn) {
+ request = await fn(request, opts);
+ }
+ }
+
+ // fetch must be assigned here, otherwise it would throw the error:
+ // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
+ const _fetch = opts.fetch!;
+ let response: Response;
+
+ try {
+ response = await _fetch(request);
+ } catch (error) {
+ // Handle fetch exceptions (AbortError, network errors, etc.)
+ let finalError = error;
+
+ for (const fn of interceptors.error.fns) {
+ if (fn) {
+ finalError = (await fn(
+ error,
+ undefined as any,
+ request,
+ opts,
+ )) as unknown;
+ }
+ }
+
+ finalError = finalError || ({} as unknown);
+
+ if (opts.throwOnError) {
+ throw finalError;
+ }
+
+ // Return error response
+ return opts.responseStyle === 'data'
+ ? undefined
+ : {
+ error: finalError,
+ request,
+ response: undefined as any,
+ };
+ }
+
+ for (const fn of interceptors.response.fns) {
+ if (fn) {
+ response = await fn(response, request, opts);
+ }
+ }
+
+ const result = {
+ request,
+ response,
+ };
+
+ if (response.ok) {
+ const parseAs =
+ (opts.parseAs === 'auto'
+ ? getParseAs(response.headers.get('Content-Type'))
+ : opts.parseAs) ?? 'json';
+
+ if (
+ response.status === 204 ||
+ response.headers.get('Content-Length') === '0'
+ ) {
+ let emptyData: any;
+ switch (parseAs) {
+ case 'arrayBuffer':
+ case 'blob':
+ case 'text':
+ emptyData = await response[parseAs]();
+ break;
+ case 'formData':
+ emptyData = new FormData();
+ break;
+ case 'stream':
+ emptyData = response.body;
+ break;
+ case 'json':
+ default:
+ emptyData = {};
+ break;
+ }
+ return opts.responseStyle === 'data'
+ ? emptyData
+ : {
+ data: emptyData,
+ ...result,
+ };
+ }
+
+ let data: any;
+ switch (parseAs) {
+ case 'arrayBuffer':
+ case 'blob':
+ case 'formData':
+ case 'json':
+ case 'text':
+ data = await response[parseAs]();
+ break;
+ case 'stream':
+ return opts.responseStyle === 'data'
+ ? response.body
+ : {
+ data: response.body,
+ ...result,
+ };
+ }
+
+ if (parseAs === 'json') {
+ if (opts.responseValidator) {
+ await opts.responseValidator(data);
+ }
+
+ if (opts.responseTransformer) {
+ data = await opts.responseTransformer(data);
+ }
+ }
+
+ return opts.responseStyle === 'data'
+ ? data
+ : {
+ data,
+ ...result,
+ };
+ }
+
+ const textError = await response.text();
+ let jsonError: unknown;
+
+ try {
+ jsonError = JSON.parse(textError);
+ } catch {
+ // noop
+ }
+
+ const error = jsonError ?? textError;
+ let finalError = error;
+
+ for (const fn of interceptors.error.fns) {
+ if (fn) {
+ finalError = (await fn(error, response, request, opts)) as string;
+ }
+ }
+
+ finalError = finalError || ({} as string);
+
+ if (opts.throwOnError) {
+ throw finalError;
+ }
+
+ // TODO: we probably want to return error and improve types
+ return opts.responseStyle === 'data'
+ ? undefined
+ : {
+ error: finalError,
+ ...result,
+ };
+ };
+
+ const makeMethodFn =
+ (method: Uppercase) => (options: RequestOptions) =>
+ request({ ...options, method });
+
+ const makeSseFn =
+ (method: Uppercase) => async (options: RequestOptions) => {
+ const { opts, url } = await beforeRequest(options);
+ return createSseClient({
+ ...opts,
+ body: opts.body as BodyInit | null | undefined,
+ headers: opts.headers as unknown as Record,
+ method,
+ onRequest: async (url, init) => {
+ let request = new Request(url, init);
+ for (const fn of interceptors.request.fns) {
+ if (fn) {
+ request = await fn(request, opts);
+ }
+ }
+ return request;
+ },
+ url,
+ });
+ };
+
+ return {
+ buildUrl,
+ connect: makeMethodFn('CONNECT'),
+ delete: makeMethodFn('DELETE'),
+ get: makeMethodFn('GET'),
+ getConfig,
+ head: makeMethodFn('HEAD'),
+ interceptors,
+ options: makeMethodFn('OPTIONS'),
+ patch: makeMethodFn('PATCH'),
+ post: makeMethodFn('POST'),
+ put: makeMethodFn('PUT'),
+ request,
+ setConfig,
+ sse: {
+ connect: makeSseFn('CONNECT'),
+ delete: makeSseFn('DELETE'),
+ get: makeSseFn('GET'),
+ head: makeSseFn('HEAD'),
+ options: makeSseFn('OPTIONS'),
+ patch: makeSseFn('PATCH'),
+ post: makeSseFn('POST'),
+ put: makeSseFn('PUT'),
+ trace: makeSseFn('TRACE'),
+ },
+ trace: makeMethodFn('TRACE'),
+ } as Client;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/swr/asClass/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/swr/asClass/client/index.ts
new file mode 100644
index 0000000000..b295edeca0
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/swr/asClass/client/index.ts
@@ -0,0 +1,25 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+export type { Auth } from '../core/auth.gen';
+export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
+export {
+ formDataBodySerializer,
+ jsonBodySerializer,
+ urlSearchParamsBodySerializer,
+} from '../core/bodySerializer.gen';
+export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
+export { createClient } from './client.gen';
+export type {
+ Client,
+ ClientOptions,
+ Config,
+ CreateClientConfig,
+ Options,
+ RequestOptions,
+ RequestResult,
+ ResolvedRequestOptions,
+ ResponseStyle,
+ TDataShape,
+} from './types.gen';
+export { createConfig, mergeHeaders } from './utils.gen';
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/swr/asClass/client/types.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/swr/asClass/client/types.gen.ts
new file mode 100644
index 0000000000..b4a499cc03
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/swr/asClass/client/types.gen.ts
@@ -0,0 +1,241 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type { Auth } from '../core/auth.gen';
+import type {
+ ServerSentEventsOptions,
+ ServerSentEventsResult,
+} from '../core/serverSentEvents.gen';
+import type {
+ Client as CoreClient,
+ Config as CoreConfig,
+} from '../core/types.gen';
+import type { Middleware } from './utils.gen';
+
+export type ResponseStyle = 'data' | 'fields';
+
+export interface Config
+ extends Omit,
+ CoreConfig {
+ /**
+ * Base URL for all requests made by this client.
+ */
+ baseUrl?: T['baseUrl'];
+ /**
+ * Fetch API implementation. You can use this option to provide a custom
+ * fetch instance.
+ *
+ * @default globalThis.fetch
+ */
+ fetch?: typeof fetch;
+ /**
+ * Please don't use the Fetch client for Next.js applications. The `next`
+ * options won't have any effect.
+ *
+ * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
+ */
+ next?: never;
+ /**
+ * Return the response data parsed in a specified format. By default, `auto`
+ * will infer the appropriate method from the `Content-Type` response header.
+ * You can override this behavior with any of the {@link Body} methods.
+ * Select `stream` if you don't want to parse response data at all.
+ *
+ * @default 'auto'
+ */
+ parseAs?:
+ | 'arrayBuffer'
+ | 'auto'
+ | 'blob'
+ | 'formData'
+ | 'json'
+ | 'stream'
+ | 'text';
+ /**
+ * Should we return only data or multiple fields (data, error, response, etc.)?
+ *
+ * @default 'fields'
+ */
+ responseStyle?: ResponseStyle;
+ /**
+ * Throw an error instead of returning it in the response?
+ *
+ * @default false
+ */
+ throwOnError?: T['throwOnError'];
+}
+
+export interface RequestOptions<
+ TData = unknown,
+ TResponseStyle extends ResponseStyle = 'fields',
+ ThrowOnError extends boolean = boolean,
+ Url extends string = string,
+> extends Config<{
+ responseStyle: TResponseStyle;
+ throwOnError: ThrowOnError;
+ }>,
+ Pick<
+ ServerSentEventsOptions,
+ | 'onSseError'
+ | 'onSseEvent'
+ | 'sseDefaultRetryDelay'
+ | 'sseMaxRetryAttempts'
+ | 'sseMaxRetryDelay'
+ > {
+ /**
+ * Any body that you want to add to your request.
+ *
+ * {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
+ */
+ body?: unknown;
+ path?: Record;
+ query?: Record;
+ /**
+ * Security mechanism(s) to use for the request.
+ */
+ security?: ReadonlyArray;
+ url: Url;
+}
+
+export interface ResolvedRequestOptions<
+ TResponseStyle extends ResponseStyle = 'fields',
+ ThrowOnError extends boolean = boolean,
+ Url extends string = string,
+> extends RequestOptions {
+ serializedBody?: string;
+}
+
+export type RequestResult<
+ TData = unknown,
+ TError = unknown,
+ ThrowOnError extends boolean = boolean,
+ TResponseStyle extends ResponseStyle = 'fields',
+> = ThrowOnError extends true
+ ? Promise<
+ TResponseStyle extends 'data'
+ ? TData extends Record
+ ? TData[keyof TData]
+ : TData
+ : {
+ data: TData extends Record
+ ? TData[keyof TData]
+ : TData;
+ request: Request;
+ response: Response;
+ }
+ >
+ : Promise<
+ TResponseStyle extends 'data'
+ ?
+ | (TData extends Record
+ ? TData[keyof TData]
+ : TData)
+ | undefined
+ : (
+ | {
+ data: TData extends Record
+ ? TData[keyof TData]
+ : TData;
+ error: undefined;
+ }
+ | {
+ data: undefined;
+ error: TError extends Record
+ ? TError[keyof TError]
+ : TError;
+ }
+ ) & {
+ request: Request;
+ response: Response;
+ }
+ >;
+
+export interface ClientOptions {
+ baseUrl?: string;
+ responseStyle?: ResponseStyle;
+ throwOnError?: boolean;
+}
+
+type MethodFn = <
+ TData = unknown,
+ TError = unknown,
+ ThrowOnError extends boolean = false,
+ TResponseStyle extends ResponseStyle = 'fields',
+>(
+ options: Omit, 'method'>,
+) => RequestResult;
+
+type SseFn = <
+ TData = unknown,
+ TError = unknown,
+ ThrowOnError extends boolean = false,
+ TResponseStyle extends ResponseStyle = 'fields',
+>(
+ options: Omit, 'method'>,
+) => Promise>;
+
+type RequestFn = <
+ TData = unknown,
+ TError = unknown,
+ ThrowOnError extends boolean = false,
+ TResponseStyle extends ResponseStyle = 'fields',
+>(
+ options: Omit, 'method'> &
+ Pick<
+ Required>,
+ 'method'
+ >,
+) => RequestResult;
+
+type BuildUrlFn = <
+ TData extends {
+ body?: unknown;
+ path?: Record;
+ query?: Record;
+ url: string;
+ },
+>(
+ options: TData & Options,
+) => string;
+
+export type Client = CoreClient<
+ RequestFn,
+ Config,
+ MethodFn,
+ BuildUrlFn,
+ SseFn
+> & {
+ interceptors: Middleware;
+};
+
+/**
+ * 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 = (
+ override?: Config,
+) => Config & T>;
+
+export interface TDataShape {
+ body?: unknown;
+ headers?: unknown;
+ path?: unknown;
+ query?: unknown;
+ url: string;
+}
+
+type OmitKeys = Pick