diff --git a/examples/openapi-ts-ofetch/env.d.ts b/examples/openapi-ts-ofetch/env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/examples/openapi-ts-ofetch/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/openapi-ts-ofetch/index.html b/examples/openapi-ts-ofetch/index.html
new file mode 100644
index 000000000..c90490343
--- /dev/null
+++ b/examples/openapi-ts-ofetch/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Hey API — ofetch example
+
+
+
+
+
+
diff --git a/examples/openapi-ts-ofetch/openapi-ts.config.ts b/examples/openapi-ts-ofetch/openapi-ts.config.ts
new file mode 100644
index 000000000..e0d23642c
--- /dev/null
+++ b/examples/openapi-ts-ofetch/openapi-ts.config.ts
@@ -0,0 +1,20 @@
+import { defineConfig } from '@hey-api/openapi-ts';
+
+export default defineConfig({
+ input:
+ 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml',
+ output: {
+ format: 'prettier',
+ lint: 'eslint',
+ path: './src/client',
+ },
+ plugins: [
+ '@hey-api/client-ofetch',
+ '@hey-api/schemas',
+ '@hey-api/sdk',
+ {
+ enums: 'javascript',
+ name: '@hey-api/typescript',
+ },
+ ],
+});
diff --git a/examples/openapi-ts-ofetch/package.json b/examples/openapi-ts-ofetch/package.json
new file mode 100644
index 000000000..6427931e6
--- /dev/null
+++ b/examples/openapi-ts-ofetch/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@example/openapi-ts-ofetch",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "scripts": {
+ "build": "vite build",
+ "dev": "vite",
+ "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx --fix --ignore-path .gitignore",
+ "openapi-ts": "openapi-ts",
+ "preview": "vite preview",
+ "typecheck": "vue-tsc --build --force"
+ },
+ "dependencies": {
+ "ofetch": "1.4.1",
+ "vue": "3.5.21"
+ },
+ "devDependencies": {
+ "@config/vite-base": "workspace:*",
+ "@hey-api/openapi-ts": "workspace:*",
+ "@rushstack/eslint-patch": "1.10.5",
+ "@tsconfig/node20": "20.1.4",
+ "@types/jsdom": "21.1.7",
+ "@types/node": "22.10.5",
+ "@vitejs/plugin-vue": "5.2.1",
+ "@vitejs/plugin-vue-jsx": "4.1.1",
+ "@vue/eslint-config-prettier": "10.1.0",
+ "@vue/eslint-config-typescript": "14.2.0",
+ "@vue/test-utils": "2.4.6",
+ "@vue/tsconfig": "0.7.0",
+ "autoprefixer": "10.4.20",
+ "eslint": "9.17.0",
+ "eslint-plugin-vue": "9.32.0",
+ "jsdom": "23.0.0",
+ "npm-run-all2": "6.2.0",
+ "postcss": "8.4.41",
+ "prettier": "3.4.2",
+ "tailwindcss": "3.4.9",
+ "typescript": "5.8.3",
+ "vite": "7.1.2",
+ "vite-plugin-vue-devtools": "7.7.0",
+ "vitest": "3.1.1",
+ "vue-tsc": "2.2.0"
+ }
+}
diff --git a/examples/openapi-ts-ofetch/postcss.config.js b/examples/openapi-ts-ofetch/postcss.config.js
new file mode 100644
index 000000000..9eef821c4
--- /dev/null
+++ b/examples/openapi-ts-ofetch/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ autoprefixer: {},
+ tailwindcss: {},
+ },
+};
diff --git a/examples/openapi-ts-ofetch/src/App.vue b/examples/openapi-ts-ofetch/src/App.vue
new file mode 100644
index 000000000..d48b4c201
--- /dev/null
+++ b/examples/openapi-ts-ofetch/src/App.vue
@@ -0,0 +1,190 @@
+
+
+
+
+
+
+
+
+
+
+
+ @hey-api/openapi-ts 🤝 ofetch
+
+
+
+
+
+
+
+ {{ pet?.name?.slice(0, 1) || 'N' }}
+
+
+
+
+
+ Name: {{ pet?.name || 'N/A' }}
+
+
+
+ Category: {{ pet?.category?.name || 'N/A' }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/openapi-ts-ofetch/src/assets/main.css b/examples/openapi-ts-ofetch/src/assets/main.css
new file mode 100644
index 000000000..0a2ac6aac
--- /dev/null
+++ b/examples/openapi-ts-ofetch/src/assets/main.css
@@ -0,0 +1,7 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ @apply bg-[#111113];
+}
diff --git a/examples/openapi-ts-ofetch/src/client/client.gen.ts b/examples/openapi-ts-ofetch/src/client/client.gen.ts
new file mode 100644
index 000000000..f1e680045
--- /dev/null
+++ b/examples/openapi-ts-ofetch/src/client/client.gen.ts
@@ -0,0 +1,28 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import {
+ type ClientOptions as DefaultClientOptions,
+ type Config,
+ createClient,
+ createConfig,
+} from './client';
+import type { ClientOptions } 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-ofetch/src/client/client/client.gen.ts b/examples/openapi-ts-ofetch/src/client/client/client.gen.ts
new file mode 100644
index 000000000..d2482df58
--- /dev/null
+++ b/examples/openapi-ts-ofetch/src/client/client/client.gen.ts
@@ -0,0 +1,236 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import { ofetch, type ResponseType as OfetchResponseType } from 'ofetch';
+
+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 {
+ buildOfetchOptions,
+ buildUrl,
+ createConfig,
+ createInterceptors,
+ isRepeatableBody,
+ mapParseAsToResponseType,
+ mergeConfigs,
+ mergeHeaders,
+ parseError,
+ parseSuccess,
+ setAuthParams,
+ wrapDataReturn,
+ wrapErrorReturn,
+} 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,
+ 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');
+ }
+
+ // Precompute network body for retries and consistent handling
+ const networkBody = getValidRequestBody(opts) as
+ | RequestInit['body']
+ | null
+ | undefined;
+
+ const url = buildUrl(opts);
+
+ return { networkBody, opts, url };
+ };
+
+ const request: Client['request'] = async (options) => {
+ const {
+ networkBody: initialNetworkBody,
+ opts,
+ url,
+ } = await beforeRequest(options as any);
+ // Compute response type mapping once
+ const ofetchResponseType: OfetchResponseType | undefined =
+ mapParseAsToResponseType(opts.parseAs, opts.responseType);
+
+ const $ofetch = opts.ofetch ?? ofetch;
+
+ // Always create Request pre-network (align with client-fetch)
+ let networkBody = initialNetworkBody;
+ const requestInit: ReqInit = {
+ body: networkBody,
+ headers: opts.headers as Headers,
+ method: opts.method,
+ redirect: 'follow',
+ signal: opts.signal,
+ };
+ let request = new Request(url, requestInit);
+
+ for (const fn of interceptors.request.fns) {
+ if (fn) {
+ request = await fn(request, opts);
+ }
+ }
+
+ // Reflect any interceptor changes into opts used for network and downstream
+ opts.headers = request.headers;
+ opts.method = request.method as Uppercase;
+ // Attempt to reflect possible signal/body changes (safely)
+
+ const reqBody = (request as any).body as unknown;
+ let effectiveRetry = opts.retry;
+ if (reqBody !== undefined && reqBody !== null) {
+ if (isRepeatableBody(reqBody)) {
+ networkBody = reqBody as BodyInit;
+ } else {
+ networkBody = reqBody as BodyInit;
+ // Disable retries for non-repeatable bodies
+ effectiveRetry = 0 as any;
+ }
+ }
+
+ opts.signal = (request as any).signal as AbortSignal | undefined;
+ const finalUrl = request.url;
+
+ // Build ofetch options and perform the request
+ const responseOptions = buildOfetchOptions(
+ opts as ResolvedRequestOptions,
+ networkBody ?? undefined,
+ effectiveRetry as any,
+ );
+
+ let response = await $ofetch.raw(finalUrl, responseOptions);
+
+ for (const fn of interceptors.response.fns) {
+ if (fn) {
+ response = await fn(response, request, opts);
+ }
+ }
+
+ const result = { request, response };
+
+ if (response.ok) {
+ const data = await parseSuccess(response, opts, ofetchResponseType);
+ return wrapDataReturn(data, result, opts.responseStyle);
+ }
+
+ let finalError = await parseError(response);
+
+ for (const fn of interceptors.error.fns) {
+ if (fn) {
+ finalError = await fn(finalError, response, request, opts);
+ }
+ }
+
+ // Ensure error is never undefined after interceptors
+ finalError = (finalError as any) || ({} as string);
+
+ if (opts.throwOnError) {
+ throw finalError;
+ }
+
+ return wrapErrorReturn(finalError, result, opts.responseStyle) as any;
+ };
+
+ const makeMethodFn =
+ (method: Uppercase) => (options: RequestOptions) =>
+ request({ ...options, method } as any);
+
+ const makeSseFn =
+ (method: Uppercase) => async (options: RequestOptions) => {
+ const { networkBody, opts, url } = await beforeRequest(options);
+ const optsForSse: any = { ...opts };
+ delete optsForSse.body;
+ return createSseClient({
+ ...optsForSse,
+ fetch: opts.fetch,
+ headers: opts.headers as Headers,
+ 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;
+ },
+ serializedBody: networkBody as BodyInit | null | undefined,
+ signal: opts.signal,
+ 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-ofetch/src/client/client/index.ts b/examples/openapi-ts-ofetch/src/client/client/index.ts
new file mode 100644
index 000000000..318a84b6a
--- /dev/null
+++ b/examples/openapi-ts-ofetch/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 { createClient } from './client.gen';
+export type {
+ Client,
+ ClientOptions,
+ Config,
+ CreateClientConfig,
+ Options,
+ OptionsLegacyParser,
+ RequestOptions,
+ RequestResult,
+ ResolvedRequestOptions,
+ ResponseStyle,
+ TDataShape,
+} from './types.gen';
+export { createConfig, mergeHeaders } from './utils.gen';
diff --git a/examples/openapi-ts-ofetch/src/client/client/types.gen.ts b/examples/openapi-ts-ofetch/src/client/client/types.gen.ts
new file mode 100644
index 000000000..e4925b81b
--- /dev/null
+++ b/examples/openapi-ts-ofetch/src/client/client/types.gen.ts
@@ -0,0 +1,300 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type {
+ FetchOptions as OfetchOptions,
+ ResponseType as OfetchResponseType,
+} from 'ofetch';
+import type { ofetch } from 'ofetch';
+
+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 {
+ agent?: OfetchOptions['agent'];
+ /**
+ * Base URL for all requests made by this client.
+ */
+ baseUrl?: T['baseUrl'];
+ /** Node-only proxy/agent options */
+ dispatcher?: OfetchOptions['dispatcher'];
+ /** Optional fetch instance used for SSE streaming */
+ fetch?: typeof fetch;
+ // No custom fetch option: provide custom instance via `ofetch` instead
+ /**
+ * 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;
+ /**
+ * Custom ofetch instance created via `ofetch.create()`. If provided, it will
+ * be used for requests instead of the default `ofetch` export.
+ */
+ ofetch?: typeof ofetch;
+ /** ofetch interceptors and runtime options */
+ onRequest?: OfetchOptions['onRequest'];
+ onRequestError?: OfetchOptions['onRequestError'];
+ onResponse?: OfetchOptions['onResponse'];
+ onResponseError?: OfetchOptions['onResponseError'];
+ /**
+ * 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';
+ /** Custom response parser (ofetch). */
+ parseResponse?: OfetchOptions['parseResponse'];
+ /**
+ * Should we return only data or multiple fields (data, error, response, etc.)?
+ *
+ * @default 'fields'
+ */
+ responseStyle?: ResponseStyle;
+ /**
+ * ofetch responseType override. If provided, it will be passed directly to
+ * ofetch and take precedence over `parseAs`.
+ */
+ responseType?: OfetchResponseType;
+ /**
+ * Automatically retry failed requests.
+ */
+ retry?: OfetchOptions['retry'];
+ retryDelay?: OfetchOptions['retryDelay'];
+ retryStatusCodes?: OfetchOptions['retryStatusCodes'];
+ /**
+ * Throw an error instead of returning it in the response?
+ *
+ * @default false
+ */
+ throwOnError?: T['throwOnError'];
+ /**
+ * Abort the request after the given milliseconds.
+ */
+ timeout?: number;
+}
+
+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: Pick & 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'
+> &
+ Omit;
+
+export type OptionsLegacyParser<
+ TData = unknown,
+ ThrowOnError extends boolean = boolean,
+ TResponseStyle extends ResponseStyle = 'fields',
+> = TData extends { body?: any }
+ ? TData extends { headers?: any }
+ ? OmitKeys<
+ RequestOptions,
+ 'body' | 'headers' | 'url'
+ > &
+ TData
+ : OmitKeys<
+ RequestOptions,
+ 'body' | 'url'
+ > &
+ TData &
+ Pick, 'headers'>
+ : TData extends { headers?: any }
+ ? OmitKeys<
+ RequestOptions,
+ 'headers' | 'url'
+ > &
+ TData &
+ Pick, 'body'>
+ : OmitKeys, 'url'> &
+ TData;
diff --git a/examples/openapi-ts-ofetch/src/client/client/utils.gen.ts b/examples/openapi-ts-ofetch/src/client/client/utils.gen.ts
new file mode 100644
index 000000000..17c601b39
--- /dev/null
+++ b/examples/openapi-ts-ofetch/src/client/client/utils.gen.ts
@@ -0,0 +1,532 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type {
+ FetchOptions as OfetchOptions,
+ ResponseType as OfetchResponseType,
+} from 'ofetch';
+
+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,
+ ResolvedRequestOptions,
+ ResponseStyle,
+} from './types.gen';
+
+export const createQuerySerializer = ({
+ allowReserved,
+ array,
+ object,
+}: 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;
+ }
+
+ if (Array.isArray(value)) {
+ const serializedArray = serializeArrayParam({
+ allowReserved,
+ explode: true,
+ name,
+ style: 'form',
+ value,
+ ...array,
+ });
+ if (serializedArray) search.push(serializedArray);
+ } else if (typeof value === 'object') {
+ const serializedObject = serializeObjectParam({
+ allowReserved,
+ explode: true,
+ name,
+ style: 'deepObject',
+ value: value as Record,
+ ...object,
+ });
+ if (serializedObject) search.push(serializedObject);
+ } else {
+ const serializedPrimitive = serializePrimitiveParam({
+ 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;
+};
+
+/**
+ * Map our parseAs value to ofetch responseType when not explicitly provided.
+ */
+export const mapParseAsToResponseType = (
+ parseAs: Config['parseAs'] | undefined,
+ explicit?: OfetchResponseType,
+): OfetchResponseType | undefined => {
+ if (explicit) return explicit;
+ switch (parseAs) {
+ case 'arrayBuffer':
+ case 'blob':
+ case 'json':
+ case 'text':
+ case 'stream':
+ return parseAs;
+ case 'formData':
+ case 'auto':
+ default:
+ return undefined; // let ofetch auto-detect
+ }
+};
+
+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;
+};
+
+/**
+ * Heuristic to detect whether a request body can be safely retried.
+ */
+export const isRepeatableBody = (body: unknown): boolean => {
+ if (body == null) return true; // undefined/null treated as no-body
+ if (typeof body === 'string') return true;
+ if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams)
+ return true;
+ if (typeof Uint8Array !== 'undefined' && body instanceof Uint8Array)
+ return true;
+ if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer)
+ return true;
+ if (typeof Blob !== 'undefined' && body instanceof Blob) return true;
+ if (typeof FormData !== 'undefined' && body instanceof FormData) return true;
+ // Streams are not repeatable
+ if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream)
+ return false;
+ // Default: assume non-repeatable for unknown structured bodies
+ return false;
+};
+
+/**
+ * Small helper to unify data vs fields return style.
+ */
+export const wrapDataReturn = (
+ data: T,
+ result: { request: Request; response: Response },
+ responseStyle: ResponseStyle | undefined,
+):
+ | T
+ | ((T extends Record ? { data: T } : { data: T }) &
+ typeof result) =>
+ (responseStyle ?? 'fields') === 'data'
+ ? (data as any)
+ : ({ data, ...result } as any);
+
+/**
+ * Small helper to unify error vs fields return style.
+ */
+export const wrapErrorReturn = (
+ error: E,
+ result: { request: Request; response: Response },
+ responseStyle: ResponseStyle | undefined,
+):
+ | undefined
+ | ((E extends Record ? { error: E } : { error: E }) &
+ typeof result) =>
+ (responseStyle ?? 'fields') === 'data'
+ ? undefined
+ : ({ error, ...result } as any);
+
+/**
+ * Build options for $ofetch.raw from our resolved opts and body.
+ */
+export const buildOfetchOptions = (
+ opts: ResolvedRequestOptions,
+ body: BodyInit | null | undefined,
+ retryOverride?: OfetchOptions['retry'],
+): OfetchOptions => {
+ const responseType = mapParseAsToResponseType(
+ opts.parseAs,
+ opts.responseType,
+ );
+ return {
+ agent: opts.agent as OfetchOptions['agent'],
+ body: body as any,
+ dispatcher: opts.dispatcher as OfetchOptions['dispatcher'],
+ headers: opts.headers as Headers,
+ method: opts.method,
+ onRequest: opts.onRequest as OfetchOptions['onRequest'],
+ onRequestError: opts.onRequestError as OfetchOptions['onRequestError'],
+ onResponse: opts.onResponse as OfetchOptions['onResponse'],
+ onResponseError: opts.onResponseError as OfetchOptions['onResponseError'],
+ parseResponse: opts.parseResponse as OfetchOptions['parseResponse'],
+ query: undefined, // URL already includes query
+ responseType,
+ retry: (retryOverride ??
+ (opts.retry as OfetchOptions['retry'])) as OfetchOptions['retry'],
+ retryDelay: opts.retryDelay as OfetchOptions['retryDelay'],
+ retryStatusCodes:
+ opts.retryStatusCodes as OfetchOptions['retryStatusCodes'],
+ signal: opts.signal,
+ timeout: opts.timeout as number | undefined,
+ } as OfetchOptions;
+};
+
+/**
+ * Parse a successful response, handling empty bodies and stream cases.
+ */
+export const parseSuccess = async (
+ response: Response,
+ opts: ResolvedRequestOptions,
+ ofetchResponseType?: OfetchResponseType,
+): Promise => {
+ // Stream requested: return stream body
+ if (ofetchResponseType === 'stream') {
+ return response.body;
+ }
+
+ const inferredParseAs =
+ (opts.parseAs === 'auto'
+ ? getParseAs(response.headers.get('Content-Type'))
+ : opts.parseAs) ?? 'json';
+
+ // Handle empty responses
+ if (
+ response.status === 204 ||
+ response.headers.get('Content-Length') === '0'
+ ) {
+ switch (inferredParseAs) {
+ case 'arrayBuffer':
+ case 'blob':
+ case 'text':
+ return await (response as any)[inferredParseAs]();
+ case 'formData':
+ return new FormData();
+ case 'stream':
+ return response.body;
+ default:
+ return {};
+ }
+ }
+
+ // Prefer ofetch-populated data
+ let data: unknown = (response as any)._data;
+ if (typeof data === 'undefined') {
+ switch (inferredParseAs) {
+ case 'arrayBuffer':
+ case 'blob':
+ case 'formData':
+ case 'json':
+ case 'text':
+ data = await (response as any)[inferredParseAs]();
+ break;
+ case 'stream':
+ return response.body;
+ }
+ }
+
+ if (inferredParseAs === 'json') {
+ if (opts.responseValidator) {
+ await opts.responseValidator(data);
+ }
+ if (opts.responseTransformer) {
+ data = await opts.responseTransformer(data);
+ }
+ }
+
+ return data;
+};
+
+/**
+ * Parse an error response payload.
+ */
+export const parseError = async (response: Response): Promise => {
+ let error: unknown = (response as any)._data;
+ if (typeof error === 'undefined') {
+ const textError = await response.text();
+ try {
+ error = JSON.parse(textError);
+ } catch {
+ error = textError;
+ }
+ }
+ return error ?? ({} as string);
+};
+
+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-ofetch/src/client/core/auth.gen.ts b/examples/openapi-ts-ofetch/src/client/core/auth.gen.ts
new file mode 100644
index 000000000..f8a73266f
--- /dev/null
+++ b/examples/openapi-ts-ofetch/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-ofetch/src/client/core/bodySerializer.gen.ts b/examples/openapi-ts-ofetch/src/client/core/bodySerializer.gen.ts
new file mode 100644
index 000000000..49cd8925e
--- /dev/null
+++ b/examples/openapi-ts-ofetch/src/client/core/bodySerializer.gen.ts
@@ -0,0 +1,92 @@
+// 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;
+
+export interface QuerySerializerOptions {
+ allowReserved?: boolean;
+ array?: SerializerOptions;
+ object?: SerializerOptions;
+}
+
+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-ofetch/src/client/core/params.gen.ts b/examples/openapi-ts-ofetch/src/client/core/params.gen.ts
new file mode 100644
index 000000000..71c88e852
--- /dev/null
+++ b/examples/openapi-ts-ofetch/src/client/core/params.gen.ts
@@ -0,0 +1,153 @@
+// 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;
+ };
+
+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;
+ }
+>;
+
+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 (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;
+ (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) {
+ const name = field.map || key;
+ (params[field.in] as Record)[name] = 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 {
+ 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-ofetch/src/client/core/pathSerializer.gen.ts b/examples/openapi-ts-ofetch/src/client/core/pathSerializer.gen.ts
new file mode 100644
index 000000000..8d9993104
--- /dev/null
+++ b/examples/openapi-ts-ofetch/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-ofetch/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-ofetch/src/client/core/serverSentEvents.gen.ts
new file mode 100644
index 000000000..f8fd78e28
--- /dev/null
+++ b/examples/openapi-ts-ofetch/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-ofetch/src/client/core/types.gen.ts b/examples/openapi-ts-ofetch/src/client/core/types.gen.ts
new file mode 100644
index 000000000..643c070c9
--- /dev/null
+++ b/examples/openapi-ts-ofetch/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-ofetch/src/client/core/utils.gen.ts b/examples/openapi-ts-ofetch/src/client/core/utils.gen.ts
new file mode 100644
index 000000000..0b5389d08
--- /dev/null
+++ b/examples/openapi-ts-ofetch/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-ofetch/src/client/index.ts b/examples/openapi-ts-ofetch/src/client/index.ts
new file mode 100644
index 000000000..f796d2cc8
--- /dev/null
+++ b/examples/openapi-ts-ofetch/src/client/index.ts
@@ -0,0 +1,4 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+export * from './sdk.gen';
+export * from './types.gen';
diff --git a/examples/openapi-ts-ofetch/src/client/schemas.gen.ts b/examples/openapi-ts-ofetch/src/client/schemas.gen.ts
new file mode 100644
index 000000000..646632e83
--- /dev/null
+++ b/examples/openapi-ts-ofetch/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-ofetch/src/client/sdk.gen.ts b/examples/openapi-ts-ofetch/src/client/sdk.gen.ts
new file mode 100644
index 000000000..214fbf715
--- /dev/null
+++ b/examples/openapi-ts-ofetch/src/client/sdk.gen.ts
@@ -0,0 +1,467 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type { Client, Options as ClientOptions, 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,
+> = ClientOptions & {
+ /**
+ * 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-ofetch/src/client/types.gen.ts b/examples/openapi-ts-ofetch/src/client/types.gen.ts
new file mode 100644
index 000000000..992c17fb2
--- /dev/null
+++ b/examples/openapi-ts-ofetch/src/client/types.gen.ts
@@ -0,0 +1,699 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+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;
+};
+
+export type ClientOptions = {
+ baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {});
+};
diff --git a/examples/openapi-ts-ofetch/src/main.ts b/examples/openapi-ts-ofetch/src/main.ts
new file mode 100644
index 000000000..a4e1dc9de
--- /dev/null
+++ b/examples/openapi-ts-ofetch/src/main.ts
@@ -0,0 +1,18 @@
+import './assets/main.css';
+
+import { createApp } from 'vue';
+
+import App from './App.vue';
+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 ',
+ },
+});
+
+createApp(App).mount('#app');
diff --git a/examples/openapi-ts-ofetch/tailwind.config.ts b/examples/openapi-ts-ofetch/tailwind.config.ts
new file mode 100644
index 000000000..4cbe78a03
--- /dev/null
+++ b/examples/openapi-ts-ofetch/tailwind.config.ts
@@ -0,0 +1,9 @@
+import type { Config } from 'tailwindcss';
+
+export default {
+ content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
+ plugins: [],
+ theme: {
+ extend: {},
+ },
+} satisfies Config;
diff --git a/examples/openapi-ts-ofetch/tsconfig.app.json b/examples/openapi-ts-ofetch/tsconfig.app.json
new file mode 100644
index 000000000..81efb53e9
--- /dev/null
+++ b/examples/openapi-ts-ofetch/tsconfig.app.json
@@ -0,0 +1,14 @@
+{
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
+ "include": ["./env.d.ts", "./src/**/*", "./src/**/*.vue"],
+ "exclude": ["./src/**/__tests__/*"],
+ "compilerOptions": {
+ "composite": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/examples/openapi-ts-ofetch/tsconfig.json b/examples/openapi-ts-ofetch/tsconfig.json
new file mode 100644
index 000000000..66b5e5703
--- /dev/null
+++ b/examples/openapi-ts-ofetch/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "files": [],
+ "references": [
+ {
+ "path": "./tsconfig.node.json"
+ },
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ]
+}
diff --git a/examples/openapi-ts-ofetch/tsconfig.node.json b/examples/openapi-ts-ofetch/tsconfig.node.json
new file mode 100644
index 000000000..f09406303
--- /dev/null
+++ b/examples/openapi-ts-ofetch/tsconfig.node.json
@@ -0,0 +1,19 @@
+{
+ "extends": "@tsconfig/node20/tsconfig.json",
+ "include": [
+ "vite.config.*",
+ "vitest.config.*",
+ "cypress.config.*",
+ "nightwatch.conf.*",
+ "playwright.config.*"
+ ],
+ "compilerOptions": {
+ "composite": true,
+ "noEmit": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "types": ["node"]
+ }
+}
diff --git a/examples/openapi-ts-ofetch/vite.config.ts b/examples/openapi-ts-ofetch/vite.config.ts
new file mode 100644
index 000000000..9f2a06a37
--- /dev/null
+++ b/examples/openapi-ts-ofetch/vite.config.ts
@@ -0,0 +1,14 @@
+import { fileURLToPath, URL } from 'node:url';
+
+import { createViteConfig } from '@config/vite-base';
+import vue from '@vitejs/plugin-vue';
+
+// https://vitejs.dev/config/
+export default createViteConfig({
+ plugins: [vue()],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ },
+ },
+});