typed-openapi 2.1.2 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -574,7 +574,21 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
574
574
  responseHeaders?: TConfig["responseHeaders"]
575
575
  };
576
576
 
577
- export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
577
+ export interface Fetcher {
578
+ decodePathParams?: (path: string, pathParams: Record<string, string>) => string
579
+ encodeSearchParams?: (searchParams: Record<string, unknown> | undefined) => URLSearchParams
580
+ //
581
+ fetch: (input: {
582
+ method: Method;
583
+ url: URL;
584
+ urlSearchParams?: URLSearchParams | undefined;
585
+ parameters?: EndpointParameters | undefined;
586
+ path: string;
587
+ overrides?: RequestInit;
588
+ throwOnStatusError?: boolean
589
+ }) => Promise<Response>;
590
+ parseResponseData?: (response: Response) => Promise<unknown>
591
+ }
578
592
 
579
593
  export const successStatusCodes = [${ctx.successStatusCodes.join(",")}] as const;
580
594
  export type SuccessStatusCode = typeof successStatusCodes[number];
@@ -647,9 +661,12 @@ type RequiredKeys<T> = {
647
661
  }[keyof T];
648
662
 
649
663
  type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
664
+ type NotNever<T> = [T] extends [never] ? false : true;
650
665
 
651
666
  // </ApiClientTypes>
652
667
  `;
668
+ const infer = inferByRuntime[ctx.runtime];
669
+ const InferTEndpoint = match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`)).otherwise(() => `TEndpoint`);
653
670
  const apiClient = `
654
671
  // <TypedResponseError>
655
672
  export class TypedResponseError extends Error {
@@ -663,6 +680,7 @@ export class TypedResponseError extends Error {
663
680
  }
664
681
  }
665
682
  // </TypedResponseError>
683
+
666
684
  // <ApiClient>
667
685
  export class ApiClient {
668
686
  baseUrl: string = "";
@@ -676,64 +694,86 @@ export class ApiClient {
676
694
  return this;
677
695
  }
678
696
 
679
- parseResponse = async <T>(response: Response): Promise<T> => {
680
- const contentType = response.headers.get('content-type');
681
- if (contentType?.includes('application/json')) {
682
- return response.json();
697
+ /**
698
+ * Replace path parameters in URL
699
+ * Supports both OpenAPI format {param} and Express format :param
700
+ */
701
+ defaultDecodePathParams = (url: string, params: Record<string, string>): string => {
702
+ return url
703
+ .replace(/{(\\w+)}/g, (_, key: string) => params[key] || \`{\${key}}\`)
704
+ .replace(/:([a-zA-Z0-9_]+)/g, (_, key: string) => params[key] || \`:\${key}\`);
705
+ }
706
+
707
+ /** Uses URLSearchParams, skips null/undefined values */
708
+ defaultEncodeSearchParams = (queryParams: Record<string, unknown> | undefined): URLSearchParams | undefined => {
709
+ if (!queryParams) return;
710
+
711
+ const searchParams = new URLSearchParams();
712
+ Object.entries(queryParams).forEach(([key, value]) => {
713
+ if (value != null) {
714
+ // Skip null/undefined values
715
+ if (Array.isArray(value)) {
716
+ value.forEach((val) => val != null && searchParams.append(key, String(val)));
717
+ } else {
718
+ searchParams.append(key, String(value));
719
+ }
720
+ }
721
+ });
722
+
723
+ return searchParams;
724
+ }
725
+
726
+ defaultParseResponseData = async (response: Response): Promise<unknown> => {
727
+ const contentType = response.headers.get("content-type") ?? "";
728
+ if (contentType.startsWith("text/")) {
729
+ return (await response.text())
730
+ }
731
+
732
+ if (contentType === "application/octet-stream") {
733
+ return (await response.arrayBuffer())
734
+ }
735
+
736
+ if (
737
+ contentType.includes("application/json") ||
738
+ (contentType.includes("application/") && contentType.includes("json")) ||
739
+ contentType === "*/*"
740
+ ) {
741
+ try {
742
+ return await response.json();
743
+ } catch {
744
+ return undefined
745
+ }
683
746
  }
684
- return response.text() as unknown as T;
747
+
748
+ return
685
749
  }
686
750
 
687
751
  ${Object.entries(byMethods).map(([method, endpointByMethod]) => {
688
752
  const capitalizedMethod = capitalize2(method);
689
- const infer = inferByRuntime[ctx.runtime];
690
753
  return endpointByMethod.length ? `// <ApiClient.${method}>
691
754
  ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
692
755
  path: Path,
693
- ...params: MaybeOptionalArg<${match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["parameters"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`).otherwise(() => `TEndpoint["parameters"]`)} & { withResponse?: false; throwOnStatusError?: boolean }>
694
- ): Promise<${match(ctx.runtime).with("zod", "yup", () => infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`)).with(
695
- "arktype",
696
- "io-ts",
697
- "typebox",
698
- "valibot",
699
- () => `InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]`
700
- ).otherwise(() => `Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]`)}>;
756
+ ...params: MaybeOptionalArg<
757
+ (TEndpoint extends { parameters: infer UParams }
758
+ ? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
759
+ : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean })
760
+ >
761
+ ): Promise<Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]>;
701
762
 
702
763
  ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
703
764
  path: Path,
704
- ...params: MaybeOptionalArg<${match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["parameters"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`).otherwise(() => `TEndpoint["parameters"]`)} & { withResponse: true; throwOnStatusError?: boolean }>
765
+ ...params: MaybeOptionalArg<
766
+ (TEndpoint extends { parameters: infer UParams }
767
+ ? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
768
+ : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean })
769
+ >
705
770
  ): Promise<SafeApiResponse<TEndpoint>>;
706
771
 
707
- ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
772
+ ${method}<Path extends keyof ${capitalizedMethod}Endpoints, _TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
708
773
  path: Path,
709
774
  ...params: MaybeOptionalArg<any>
710
775
  ): Promise<any> {
711
- const requestParams = params[0];
712
- const withResponse = requestParams?.withResponse;
713
- const { withResponse: _, throwOnStatusError = withResponse ? false : true, ...fetchParams } = requestParams || {};
714
-
715
- const promise = this.fetcher("${method}", this.baseUrl + path, Object.keys(fetchParams).length ? requestParams : undefined)
716
- .then(async (response) => {
717
- const data = await this.parseResponse(response);
718
- const typedResponse = Object.assign(response, {
719
- data: data,
720
- json: () => Promise.resolve(data)
721
- }) as SafeApiResponse<TEndpoint>;
722
-
723
- if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
724
- throw new TypedResponseError(typedResponse as never);
725
- }
726
-
727
- return withResponse ? typedResponse : data;
728
- });
729
-
730
- return promise ${match(ctx.runtime).with("zod", "yup", () => `as Promise<${infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`)}>`).with(
731
- "arktype",
732
- "io-ts",
733
- "typebox",
734
- "valibot",
735
- () => `as Promise<InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]>`
736
- ).otherwise(() => `as Promise<Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>`)}
776
+ return this.request("${method}", path, ...params);
737
777
  }
738
778
  // </ApiClient.${method}>
739
779
  ` : "";
@@ -750,19 +790,74 @@ export class ApiClient {
750
790
  >(
751
791
  method: TMethod,
752
792
  path: TPath,
753
- ...params: MaybeOptionalArg<${match(ctx.runtime).with(
754
- "zod",
755
- "yup",
756
- () => inferByRuntime[ctx.runtime](`TEndpoint extends { parameters: infer Params } ? Params : never`)
757
- ).with(
758
- "arktype",
759
- "io-ts",
760
- "typebox",
761
- "valibot",
762
- () => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`
763
- ).otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
764
- : Promise<SafeApiResponse<TEndpoint>> {
765
- return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters) as Promise<SafeApiResponse<TEndpoint>>;
793
+ ...params: MaybeOptionalArg<
794
+ (TEndpoint extends { parameters: infer UParams }
795
+ ? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
796
+ : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean })
797
+ >
798
+ ): Promise<Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]>
799
+
800
+ request<
801
+ TMethod extends keyof EndpointByMethod,
802
+ TPath extends keyof EndpointByMethod[TMethod],
803
+ TEndpoint extends EndpointByMethod[TMethod][TPath]
804
+ >(
805
+ method: TMethod,
806
+ path: TPath,
807
+ ...params: MaybeOptionalArg<
808
+ (TEndpoint extends { parameters: infer UParams }
809
+ ? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
810
+ : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean })
811
+ >
812
+ ): Promise<SafeApiResponse<TEndpoint>>;
813
+
814
+ request<
815
+ TMethod extends keyof EndpointByMethod,
816
+ TPath extends keyof EndpointByMethod[TMethod],
817
+ TEndpoint extends EndpointByMethod[TMethod][TPath]
818
+ >(
819
+ method: TMethod,
820
+ path: TPath,
821
+ ...params: MaybeOptionalArg<any>
822
+ ): Promise<any> {
823
+ const requestParams = params[0];
824
+ const withResponse = requestParams?.withResponse;
825
+ const { withResponse: _, throwOnStatusError = withResponse ? false : true, overrides, ...fetchParams } = requestParams || {};
826
+
827
+ const parametersToSend: EndpointParameters = {};
828
+ if (requestParams?.body !== undefined) (parametersToSend as any).body = requestParams.body;
829
+ if (requestParams?.query !== undefined) (parametersToSend as any).query = requestParams.query;
830
+ if (requestParams?.header !== undefined) (parametersToSend as any).header = requestParams.header;
831
+ if (requestParams?.path !== undefined) (parametersToSend as any).path = requestParams.path;
832
+
833
+ const resolvedPath = (this.fetcher.decodePathParams ?? this.defaultDecodePathParams)(this.baseUrl + (path as string), (parametersToSend.path ?? {}) as Record<string, string>);
834
+ const url = new URL(resolvedPath);
835
+ const urlSearchParams = (this.fetcher.encodeSearchParams ?? this.defaultEncodeSearchParams)(parametersToSend.query);
836
+
837
+ const promise = this.fetcher.fetch({
838
+ method: method,
839
+ path: (path as string),
840
+ url,
841
+ urlSearchParams,
842
+ parameters: Object.keys(fetchParams).length ? fetchParams : undefined,
843
+ overrides,
844
+ throwOnStatusError
845
+ })
846
+ .then(async (response) => {
847
+ const data = await (this.fetcher.parseResponseData ?? this.defaultParseResponseData)(response);
848
+ const typedResponse = Object.assign(response, {
849
+ data: data,
850
+ json: () => Promise.resolve(data)
851
+ }) as SafeApiResponse<TEndpoint>;
852
+
853
+ if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
854
+ throw new TypedResponseError(typedResponse as never);
855
+ }
856
+
857
+ return withResponse ? typedResponse : data;
858
+ });
859
+
860
+ return promise as Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]
766
861
  }
767
862
  // </ApiClient.request>
768
863
  }
@@ -1171,7 +1266,7 @@ var mapOpenApiEndpoints = (doc, options) => {
1171
1266
  endpoint.parameters = Object.keys(params).length ? params : void 0;
1172
1267
  }
1173
1268
  const allResponses = {};
1174
- const allHeaders = {};
1269
+ const allResponseHeaders = {};
1175
1270
  Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
1176
1271
  const responseObj = refs.unwrap(responseOrRef);
1177
1272
  const content = responseObj?.content;
@@ -1179,11 +1274,19 @@ var mapOpenApiEndpoints = (doc, options) => {
1179
1274
  if (content && mediaTypes.length) {
1180
1275
  mediaTypes.forEach((mediaType) => {
1181
1276
  const schema = content[mediaType] ? content[mediaType].schema ?? {} : {};
1182
- const t2 = createBoxFactory(schema, ctx);
1183
1277
  const mediaTypeResponse = openApiSchemaToTs({ schema, ctx });
1184
1278
  if (allResponses[status]) {
1279
+ const t2 = createBoxFactory(
1280
+ {
1281
+ oneOf: [
1282
+ ...allResponses[status].schema.oneOf ? allResponses[status].schema.oneOf : [allResponses[status].schema],
1283
+ schema
1284
+ ]
1285
+ },
1286
+ ctx
1287
+ );
1185
1288
  allResponses[status] = t2.union([
1186
- ...Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]],
1289
+ ...allResponses[status].type === "union" ? allResponses[status].params.types : [allResponses[status]],
1187
1290
  mediaTypeResponse
1188
1291
  ]);
1189
1292
  } else {
@@ -1193,10 +1296,18 @@ var mapOpenApiEndpoints = (doc, options) => {
1193
1296
  } else {
1194
1297
  const schema = {};
1195
1298
  const unknown = openApiSchemaToTs({ schema: {}, ctx });
1196
- const t2 = createBoxFactory(schema, ctx);
1197
1299
  if (allResponses[status]) {
1300
+ const t2 = createBoxFactory(
1301
+ {
1302
+ oneOf: [
1303
+ ...allResponses[status].schema.oneOf ? allResponses[status].schema.oneOf : [allResponses[status].schema],
1304
+ schema
1305
+ ]
1306
+ },
1307
+ ctx
1308
+ );
1198
1309
  allResponses[status] = t2.union([
1199
- ...Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]],
1310
+ ...allResponses[status].type === "union" ? allResponses[status].params.types : [allResponses[status]],
1200
1311
  unknown
1201
1312
  ]);
1202
1313
  } else {
@@ -1219,15 +1330,15 @@ var mapOpenApiEndpoints = (doc, options) => {
1219
1330
  {}
1220
1331
  );
1221
1332
  if (Object.keys(mappedHeaders).length) {
1222
- allHeaders[status] = t.object(mappedHeaders);
1333
+ allResponseHeaders[status] = t.object(mappedHeaders);
1223
1334
  }
1224
1335
  }
1225
1336
  });
1226
1337
  if (Object.keys(allResponses).length > 0) {
1227
1338
  endpoint.responses = allResponses;
1228
1339
  }
1229
- if (Object.keys(allHeaders).length) {
1230
- endpoint.responseHeaders = allHeaders;
1340
+ if (Object.keys(allResponseHeaders).length) {
1341
+ endpoint.responseHeaders = allResponseHeaders;
1231
1342
  }
1232
1343
  endpointList.push(endpoint);
1233
1344
  });
@@ -1253,7 +1364,7 @@ var generateTanstackQueryFile = async (ctx) => {
1253
1364
  const endpointMethods = new Set(ctx.endpointList.map((endpoint) => endpoint.method.toLowerCase()));
1254
1365
  const file = `
1255
1366
  import { queryOptions } from "@tanstack/react-query"
1256
- import type { EndpointByMethod, ApiClient, SuccessStatusCode, ErrorStatusCode, InferResponseByStatus } from "${ctx.relativeApiClientPath}"
1367
+ import type { EndpointByMethod, ApiClient, SuccessStatusCode, ErrorStatusCode, InferResponseByStatus, TypedSuccessResponse } from "${ctx.relativeApiClientPath}"
1257
1368
  import { errorStatusCodes, TypedResponseError } from "${ctx.relativeApiClientPath}"
1258
1369
 
1259
1370
  type EndpointQueryKey<TOptions extends EndpointParameters> = [
@@ -1305,6 +1416,11 @@ var generateTanstackQueryFile = async (ctx) => {
1305
1416
 
1306
1417
  type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
1307
1418
 
1419
+ type InferResponseData<TEndpoint, TStatusCode> = TypedSuccessResponse<any, any, any> extends
1420
+ InferResponseByStatus<TEndpoint, TStatusCode>
1421
+ ? Extract<InferResponseByStatus<TEndpoint, TStatusCode>, { data: {}}>["data"]
1422
+ : Extract<InferResponseByStatus<TEndpoint, TStatusCode>["data"], {}>;
1423
+
1308
1424
  // </ApiClientTypes>
1309
1425
 
1310
1426
  // <ApiClient>
@@ -1314,7 +1430,10 @@ var generateTanstackQueryFile = async (ctx) => {
1314
1430
  ${Array.from(endpointMethods).map(
1315
1431
  (method) => `
1316
1432
  // <ApiClient.${method}>
1317
- ${method}<Path extends keyof ${capitalize4(method)}Endpoints, TEndpoint extends ${capitalize4(method)}Endpoints[Path]>(
1433
+ ${method}<
1434
+ Path extends keyof ${capitalize4(method)}Endpoints,
1435
+ TEndpoint extends ${capitalize4(method)}Endpoints[Path]
1436
+ >(
1318
1437
  path: Path,
1319
1438
  ...params: MaybeOptionalArg<TEndpoint["parameters"]>
1320
1439
  ) {
@@ -1329,28 +1448,14 @@ var generateTanstackQueryFile = async (ctx) => {
1329
1448
  const requestParams = {
1330
1449
  ...(params[0] || {}),
1331
1450
  ...(queryKey[0] || {}),
1332
- signal,
1451
+ overrides: { signal },
1333
1452
  withResponse: false as const
1334
1453
  };
1335
- const res = await this.client.${method}(path, requestParams);
1336
- return res as Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"];
1454
+ const res = await this.client.${method}(path, requestParams as never);
1455
+ return res as InferResponseData<TEndpoint, SuccessStatusCode>;
1337
1456
  },
1338
1457
  queryKey: queryKey
1339
1458
  }),
1340
- mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook",
1341
- mutationOptions: {
1342
- mutationKey: queryKey,
1343
- mutationFn: async (localOptions: TEndpoint extends { parameters: infer Parameters} ? Parameters: never) => {
1344
- const requestParams = {
1345
- ...(params[0] || {}),
1346
- ...(queryKey[0] || {}),
1347
- ...(localOptions || {}),
1348
- withResponse: false as const
1349
- };
1350
- const res = await this.client.${method}(path, requestParams);
1351
- return res as Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"];
1352
- }
1353
- }
1354
1459
  };
1355
1460
 
1356
1461
  return query
@@ -1371,7 +1476,7 @@ var generateTanstackQueryFile = async (ctx) => {
1371
1476
  TWithResponse extends boolean = false,
1372
1477
  TSelection = TWithResponse extends true
1373
1478
  ? InferResponseByStatus<TEndpoint, SuccessStatusCode>
1374
- : Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"],
1479
+ : InferResponseData<TEndpoint, SuccessStatusCode>,
1375
1480
  TError = TEndpoint extends { responses: infer TResponses }
1376
1481
  ? TResponses extends Record<string | number, unknown>
1377
1482
  ? InferResponseByStatus<TEndpoint, ErrorStatusCode>
@@ -1381,43 +1486,57 @@ var generateTanstackQueryFile = async (ctx) => {
1381
1486
  withResponse?: TWithResponse;
1382
1487
  selectFn?: (res: TWithResponse extends true
1383
1488
  ? InferResponseByStatus<TEndpoint, SuccessStatusCode>
1384
- : Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]
1489
+ : InferResponseData<TEndpoint, SuccessStatusCode>
1385
1490
  ) => TSelection;
1386
1491
  throwOnStatusError?: boolean
1492
+ throwOnError?: boolean | ((error: TError) => boolean)
1387
1493
  }) {
1388
1494
  const mutationKey = [{ method, path }] as const;
1495
+ const mutationFn = async <TLocalWithResponse extends boolean = TWithResponse, TLocalSelection = TLocalWithResponse extends true
1496
+ ? InferResponseByStatus<TEndpoint, SuccessStatusCode>
1497
+ : InferResponseData<TEndpoint, SuccessStatusCode>>
1498
+ (params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
1499
+ withResponse?: TLocalWithResponse;
1500
+ throwOnStatusError?: boolean;
1501
+ overrides?: RequestInit;
1502
+ }): Promise<TLocalSelection> => {
1503
+ const withResponse = params.withResponse ??options?.withResponse ?? false;
1504
+ const throwOnStatusError = params.throwOnStatusError ?? options?.throwOnStatusError ?? (withResponse ? false : true);
1505
+ const selectFn = options?.selectFn;
1506
+ const response = await (this.client as any)[method](path, {
1507
+ ...params as any,
1508
+ withResponse: true,
1509
+ throwOnStatusError: false,
1510
+ });
1511
+
1512
+ if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
1513
+ throw new TypedResponseError(response as never);
1514
+ }
1515
+
1516
+ // Return just the data if withResponse is false, otherwise return the full response
1517
+ const finalResponse = withResponse ? response : response.data;
1518
+ const res = selectFn ? selectFn(finalResponse as any) : finalResponse;
1519
+ return res as never;
1520
+ };
1389
1521
  return {
1390
1522
  /** type-only property if you need easy access to the endpoint params */
1391
1523
  "~endpoint": {} as TEndpoint,
1392
1524
  mutationKey: mutationKey,
1393
1525
  mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook",
1394
1526
  mutationOptions: {
1527
+ throwOnError: options?.throwOnError as boolean | ((error: TError) => boolean),
1395
1528
  mutationKey: mutationKey,
1396
- mutationFn: async <TLocalWithResponse extends boolean = TWithResponse, TLocalSelection = TLocalWithResponse extends true
1397
- ? InferResponseByStatus<TEndpoint, SuccessStatusCode>
1398
- : Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>
1399
- (params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
1400
- withResponse?: TLocalWithResponse;
1401
- throwOnStatusError?: boolean;
1402
- }): Promise<TLocalSelection> => {
1403
- const withResponse = params.withResponse ??options?.withResponse ?? false;
1404
- const throwOnStatusError = params.throwOnStatusError ?? options?.throwOnStatusError ?? (withResponse ? false : true);
1405
- const selectFn = options?.selectFn;
1406
- const response = await (this.client as any)[method](path, { ...params as any, withResponse: true, throwOnStatusError: false });
1407
-
1408
- if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
1409
- throw new TypedResponseError(response as never);
1410
- }
1411
-
1412
- // Return just the data if withResponse is false, otherwise return the full response
1413
- const finalResponse = withResponse ? response : response.data;
1414
- const res = selectFn ? selectFn(finalResponse as any) : finalResponse;
1415
- return res as never;
1529
+ mutationFn: mutationFn,
1530
+ } as Omit<import("@tanstack/react-query").UseMutationOptions<
1531
+ TSelection,
1532
+ TError,
1533
+ (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
1534
+ withResponse?: boolean;
1535
+ throwOnStatusError?: boolean;
1416
1536
  }
1417
- } satisfies import("@tanstack/react-query").UseMutationOptions<TSelection, TError, (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
1418
- withResponse?: boolean;
1419
- throwOnStatusError?: boolean;
1420
- }>,
1537
+ >, "mutationFn"> & {
1538
+ mutationFn: typeof mutationFn
1539
+ },
1421
1540
  }
1422
1541
  }
1423
1542
  // </ApiClient.request>
@@ -5,7 +5,7 @@ import {
5
5
  generateFile,
6
6
  generateTanstackQueryFile,
7
7
  mapOpenApiEndpoints
8
- } from "./chunk-X4A4YQN7.js";
8
+ } from "./chunk-ISAFCW3H.js";
9
9
  import {
10
10
  prettify
11
11
  } from "./chunk-KAEXXJ7X.js";
@@ -35,13 +35,12 @@ var generateDefaultFetcher = (options) => {
35
35
  * - Basic error handling
36
36
  *
37
37
  * Usage:
38
- * 1. Replace './generated/api' with your actual generated file path
38
+ * 1. Replace './${clientPath}' with your actual generated file path
39
39
  * 2. Set your ${envApiBaseUrl}
40
40
  * 3. Customize error handling and headers as needed
41
41
  */
42
42
 
43
- // @ts-ignore
44
- import { type Fetcher, createApiClient } from "./${clientPath}";
43
+ import { type Fetcher, createApiClient } from "${clientPath}";
45
44
 
46
45
  // Basic configuration
47
46
  const ${envApiBaseUrl} = process.env["${envApiBaseUrl}"] || "https://api.example.com";
@@ -49,32 +48,17 @@ const ${envApiBaseUrl} = process.env["${envApiBaseUrl}"] || "https://api.example
49
48
  /**
50
49
  * Simple fetcher implementation without external dependencies
51
50
  */
52
- export const ${fetcherName}: Fetcher = async (method, apiUrl, params) => {
51
+ const ${fetcherName}: Fetcher["fetch"] = async (input) => {
53
52
  const headers = new Headers();
54
53
 
55
- // Replace path parameters (supports both {param} and :param formats)
56
- const actualUrl = replacePathParams(apiUrl, (params?.path ?? {}) as Record<string, string>);
57
- const url = new URL(actualUrl);
58
-
59
54
  // Handle query parameters
60
- if (params?.query) {
61
- const searchParams = new URLSearchParams();
62
- Object.entries(params.query).forEach(([key, value]) => {
63
- if (value != null) {
64
- // Skip null/undefined values
65
- if (Array.isArray(value)) {
66
- value.forEach((val) => val != null && searchParams.append(key, String(val)));
67
- } else {
68
- searchParams.append(key, String(value));
69
- }
70
- }
71
- });
72
- url.search = searchParams.toString();
55
+ if (input.urlSearchParams) {
56
+ input.url.search = input.urlSearchParams.toString();
73
57
  }
74
58
 
75
59
  // Handle request body for mutation methods
76
- const body = ["post", "put", "patch", "delete"].includes(method.toLowerCase())
77
- ? JSON.stringify(params?.body)
60
+ const body = ["post", "put", "patch", "delete"].includes(input.method.toLowerCase())
61
+ ? JSON.stringify(input.parameters?.body)
78
62
  : undefined;
79
63
 
80
64
  if (body) {
@@ -82,35 +66,25 @@ export const ${fetcherName}: Fetcher = async (method, apiUrl, params) => {
82
66
  }
83
67
 
84
68
  // Add custom headers
85
- if (params?.header) {
86
- Object.entries(params.header).forEach(([key, value]) => {
69
+ if (input.parameters?.header) {
70
+ Object.entries(input.parameters.header).forEach(([key, value]) => {
87
71
  if (value != null) {
88
72
  headers.set(key, String(value));
89
73
  }
90
74
  });
91
75
  }
92
76
 
93
- const response = await fetch(url, {
94
- method: method.toUpperCase(),
77
+ const response = await fetch(input.url, {
78
+ method: input.method.toUpperCase(),
95
79
  ...(body && { body }),
96
80
  headers,
81
+ ...input.overrides,
97
82
  });
98
83
 
99
84
  return response;
100
85
  };
101
86
 
102
- /**
103
- * Replace path parameters in URL
104
- * Supports both OpenAPI format {param} and Express format :param
105
- */
106
- export function replacePathParams(url: string, params: Record<string, string>): string {
107
- return url
108
- .replace(/{(\\w+)}/g, function(_, key) { return params[key] || '{' + key + '}'; })
109
- .replace(/:([a-zA-Z0-9_]+)/g, function(_, key) { return params[key] || ':' + key; });
110
- }
111
-
112
- export const ${apiName} = createApiClient(${fetcherName}, API_BASE_URL);
113
- `;
87
+ export const ${apiName} = createApiClient({ fetch: ${fetcherName} }, API_BASE_URL);`;
114
88
  };
115
89
 
116
90
  // src/generate-client-files.ts
@@ -183,7 +157,7 @@ async function generateClientFiles(input, options) {
183
157
  if (options.defaultFetcher) {
184
158
  const defaultFetcherContent = generateDefaultFetcher({
185
159
  envApiBaseUrl: options.defaultFetcher.envApiBaseUrl,
186
- clientPath: options.defaultFetcher.clientPath ?? basename(outputPath),
160
+ clientPath: options.defaultFetcher.clientPath ?? join(dirname(outputPath), basename(outputPath)),
187
161
  fetcherName: options.defaultFetcher.fetcherName,
188
162
  apiName: options.defaultFetcher.apiName
189
163
  });
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  generateClientFiles
3
- } from "./chunk-UNKLDND3.js";
3
+ } from "./chunk-TC2ECJEG.js";
4
4
  import {
5
5
  allowedRuntimes
6
- } from "./chunk-X4A4YQN7.js";
6
+ } from "./chunk-ISAFCW3H.js";
7
7
  import "./chunk-KAEXXJ7X.js";
8
8
 
9
9
  // src/cli.ts
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  openApiSchemaToTs,
9
9
  tsFactory,
10
10
  unwrap
11
- } from "./chunk-X4A4YQN7.js";
11
+ } from "./chunk-ISAFCW3H.js";
12
12
  import "./chunk-KAEXXJ7X.js";
13
13
  export {
14
14
  createBoxFactory,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  generateClientFiles
3
- } from "./chunk-UNKLDND3.js";
4
- import "./chunk-X4A4YQN7.js";
3
+ } from "./chunk-TC2ECJEG.js";
4
+ import "./chunk-ISAFCW3H.js";
5
5
  import "./chunk-KAEXXJ7X.js";
6
6
  export {
7
7
  generateClientFiles
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "typed-openapi",
3
3
  "type": "module",
4
- "version": "2.1.2",
4
+ "version": "2.2.1",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "exports": {
@@ -34,6 +34,7 @@
34
34
  "@types/node": "^22.15.17",
35
35
  "@types/prettier": "3.0.0",
36
36
  "msw": "2.10.5",
37
+ "tstyche": "4.3.0",
37
38
  "tsup": "^8.4.0",
38
39
  "typescript": "^5.8.3",
39
40
  "vitest": "^3.1.3",
@@ -67,6 +68,7 @@
67
68
  "dev": "tsup --watch",
68
69
  "build": "tsup",
69
70
  "test": "vitest",
71
+ "test:types": "tstyche",
70
72
  "gen:runtime": "node bin.js ./tests/samples/petstore.yaml --output ./tmp/generated-client.ts --tanstack generated-tanstack.ts --default-fetcher",
71
73
  "test:runtime:run": "vitest run tests/integration-runtime-msw.test.ts",
72
74
  "test:runtime": "pnpm run gen:runtime && pnpm run test:runtime:run",
@@ -22,13 +22,12 @@ export const generateDefaultFetcher = (options: {
22
22
  * - Basic error handling
23
23
  *
24
24
  * Usage:
25
- * 1. Replace './generated/api' with your actual generated file path
25
+ * 1. Replace './${clientPath}' with your actual generated file path
26
26
  * 2. Set your ${envApiBaseUrl}
27
27
  * 3. Customize error handling and headers as needed
28
28
  */
29
29
 
30
- // @ts-ignore
31
- import { type Fetcher, createApiClient } from "./${clientPath}";
30
+ import { type Fetcher, createApiClient } from "${clientPath}";
32
31
 
33
32
  // Basic configuration
34
33
  const ${envApiBaseUrl} = process.env["${envApiBaseUrl}"] || "https://api.example.com";
@@ -36,32 +35,17 @@ const ${envApiBaseUrl} = process.env["${envApiBaseUrl}"] || "https://api.example
36
35
  /**
37
36
  * Simple fetcher implementation without external dependencies
38
37
  */
39
- export const ${fetcherName}: Fetcher = async (method, apiUrl, params) => {
38
+ const ${fetcherName}: Fetcher["fetch"] = async (input) => {
40
39
  const headers = new Headers();
41
40
 
42
- // Replace path parameters (supports both {param} and :param formats)
43
- const actualUrl = replacePathParams(apiUrl, (params?.path ?? {}) as Record<string, string>);
44
- const url = new URL(actualUrl);
45
-
46
41
  // Handle query parameters
47
- if (params?.query) {
48
- const searchParams = new URLSearchParams();
49
- Object.entries(params.query).forEach(([key, value]) => {
50
- if (value != null) {
51
- // Skip null/undefined values
52
- if (Array.isArray(value)) {
53
- value.forEach((val) => val != null && searchParams.append(key, String(val)));
54
- } else {
55
- searchParams.append(key, String(value));
56
- }
57
- }
58
- });
59
- url.search = searchParams.toString();
42
+ if (input.urlSearchParams) {
43
+ input.url.search = input.urlSearchParams.toString();
60
44
  }
61
45
 
62
46
  // Handle request body for mutation methods
63
- const body = ["post", "put", "patch", "delete"].includes(method.toLowerCase())
64
- ? JSON.stringify(params?.body)
47
+ const body = ["post", "put", "patch", "delete"].includes(input.method.toLowerCase())
48
+ ? JSON.stringify(input.parameters?.body)
65
49
  : undefined;
66
50
 
67
51
  if (body) {
@@ -69,33 +53,23 @@ export const ${fetcherName}: Fetcher = async (method, apiUrl, params) => {
69
53
  }
70
54
 
71
55
  // Add custom headers
72
- if (params?.header) {
73
- Object.entries(params.header).forEach(([key, value]) => {
56
+ if (input.parameters?.header) {
57
+ Object.entries(input.parameters.header).forEach(([key, value]) => {
74
58
  if (value != null) {
75
59
  headers.set(key, String(value));
76
60
  }
77
61
  });
78
62
  }
79
63
 
80
- const response = await fetch(url, {
81
- method: method.toUpperCase(),
64
+ const response = await fetch(input.url, {
65
+ method: input.method.toUpperCase(),
82
66
  ...(body && { body }),
83
67
  headers,
68
+ ...input.overrides,
84
69
  });
85
70
 
86
71
  return response;
87
72
  };
88
73
 
89
- /**
90
- * Replace path parameters in URL
91
- * Supports both OpenAPI format {param} and Express format :param
92
- */
93
- export function replacePathParams(url: string, params: Record<string, string>): string {
94
- return url
95
- .replace(/\{(\\w+)\}/g, function(_, key) { return params[key] || '{' + key + '}'; })
96
- .replace(/:([a-zA-Z0-9_]+)/g, function(_, key) { return params[key] || ':' + key; });
97
- }
98
-
99
- export const ${apiName} = createApiClient(${fetcherName}, API_BASE_URL);
100
- `;
74
+ export const ${apiName} = createApiClient({ fetch: ${fetcherName} }, API_BASE_URL);`;
101
75
  };
@@ -110,7 +110,7 @@ export async function generateClientFiles(input: string, options: GenerateClient
110
110
  if (options.defaultFetcher) {
111
111
  const defaultFetcherContent = generateDefaultFetcher({
112
112
  envApiBaseUrl: options.defaultFetcher.envApiBaseUrl,
113
- clientPath: options.defaultFetcher.clientPath ?? basename(outputPath),
113
+ clientPath: options.defaultFetcher.clientPath ?? join(dirname(outputPath), basename(outputPath)),
114
114
  fetcherName: options.defaultFetcher.fetcherName,
115
115
  apiName: options.defaultFetcher.apiName,
116
116
  });
package/src/generator.ts CHANGED
@@ -233,11 +233,7 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
233
233
  : "parameters: never,"
234
234
  }
235
235
  ${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""}
236
- ${
237
- endpoint.responseHeaders
238
- ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders)},`
239
- : ""
240
- }
236
+ ${endpoint.responseHeaders ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders)},` : ""}
241
237
  }\n`;
242
238
  });
243
239
 
@@ -329,7 +325,21 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
329
325
  responseHeaders?: TConfig["responseHeaders"]
330
326
  };
331
327
 
332
- export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
328
+ export interface Fetcher {
329
+ decodePathParams?: (path: string, pathParams: Record<string, string>) => string
330
+ encodeSearchParams?: (searchParams: Record<string, unknown> | undefined) => URLSearchParams
331
+ //
332
+ fetch: (input: {
333
+ method: Method;
334
+ url: URL;
335
+ urlSearchParams?: URLSearchParams | undefined;
336
+ parameters?: EndpointParameters | undefined;
337
+ path: string;
338
+ overrides?: RequestInit;
339
+ throwOnStatusError?: boolean
340
+ }) => Promise<Response>;
341
+ parseResponseData?: (response: Response) => Promise<unknown>
342
+ }
333
343
 
334
344
  export const successStatusCodes = [${ctx.successStatusCodes.join(",")}] as const;
335
345
  export type SuccessStatusCode = typeof successStatusCodes[number];
@@ -402,10 +412,17 @@ type RequiredKeys<T> = {
402
412
  }[keyof T];
403
413
 
404
414
  type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
415
+ type NotNever<T> = [T] extends [never] ? false : true;
405
416
 
406
417
  // </ApiClientTypes>
407
418
  `;
408
419
 
420
+ const infer = inferByRuntime[ctx.runtime];
421
+ const InferTEndpoint = match(ctx.runtime)
422
+ .with("zod", "yup", () => infer(`TEndpoint`))
423
+ .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`))
424
+ .otherwise(() => `TEndpoint`);
425
+
409
426
  const apiClient = `
410
427
  // <TypedResponseError>
411
428
  export class TypedResponseError extends Error {
@@ -419,6 +436,7 @@ export class TypedResponseError extends Error {
419
436
  }
420
437
  }
421
438
  // </TypedResponseError>
439
+
422
440
  // <ApiClient>
423
441
  export class ApiClient {
424
442
  baseUrl: string = "";
@@ -432,79 +450,89 @@ export class ApiClient {
432
450
  return this;
433
451
  }
434
452
 
435
- parseResponse = async <T>(response: Response): Promise<T> => {
436
- const contentType = response.headers.get('content-type');
437
- if (contentType?.includes('application/json')) {
438
- return response.json();
453
+ /**
454
+ * Replace path parameters in URL
455
+ * Supports both OpenAPI format {param} and Express format :param
456
+ */
457
+ defaultDecodePathParams = (url: string, params: Record<string, string>): string => {
458
+ return url
459
+ .replace(/{(\\w+)}/g, (_, key: string) => params[key] || \`{\${key}}\`)
460
+ .replace(/:([a-zA-Z0-9_]+)/g, (_, key: string) => params[key] || \`:\${key}\`);
461
+ }
462
+
463
+ /** Uses URLSearchParams, skips null/undefined values */
464
+ defaultEncodeSearchParams = (queryParams: Record<string, unknown> | undefined): URLSearchParams | undefined => {
465
+ if (!queryParams) return;
466
+
467
+ const searchParams = new URLSearchParams();
468
+ Object.entries(queryParams).forEach(([key, value]) => {
469
+ if (value != null) {
470
+ // Skip null/undefined values
471
+ if (Array.isArray(value)) {
472
+ value.forEach((val) => val != null && searchParams.append(key, String(val)));
473
+ } else {
474
+ searchParams.append(key, String(value));
475
+ }
476
+ }
477
+ });
478
+
479
+ return searchParams;
480
+ }
481
+
482
+ defaultParseResponseData = async (response: Response): Promise<unknown> => {
483
+ const contentType = response.headers.get("content-type") ?? "";
484
+ if (contentType.startsWith("text/")) {
485
+ return (await response.text())
486
+ }
487
+
488
+ if (contentType === "application/octet-stream") {
489
+ return (await response.arrayBuffer())
439
490
  }
440
- return response.text() as unknown as T;
491
+
492
+ if (
493
+ contentType.includes("application/json") ||
494
+ (contentType.includes("application/") && contentType.includes("json")) ||
495
+ contentType === "*/*"
496
+ ) {
497
+ try {
498
+ return await response.json();
499
+ } catch {
500
+ return undefined
501
+ }
502
+ }
503
+
504
+ return
441
505
  }
442
506
 
443
507
  ${Object.entries(byMethods)
444
508
  .map(([method, endpointByMethod]) => {
445
509
  const capitalizedMethod = capitalize(method);
446
- const infer = inferByRuntime[ctx.runtime];
447
510
 
448
511
  return endpointByMethod.length
449
512
  ? `// <ApiClient.${method}>
450
513
  ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
451
514
  path: Path,
452
- ...params: MaybeOptionalArg<${match(ctx.runtime)
453
- .with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
454
- .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
455
- .otherwise(() => `TEndpoint["parameters"]`)} & { withResponse?: false; throwOnStatusError?: boolean }>
456
- ): Promise<${match(ctx.runtime)
457
- .with("zod", "yup", () => infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`))
458
- .with(
459
- "arktype",
460
- "io-ts",
461
- "typebox",
462
- "valibot",
463
- () => `InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]`,
464
- )
465
- .otherwise(() => `Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]`)}>;
515
+ ...params: MaybeOptionalArg<
516
+ (TEndpoint extends { parameters: infer UParams }
517
+ ? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
518
+ : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean })
519
+ >
520
+ ): Promise<Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]>;
466
521
 
467
522
  ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
468
523
  path: Path,
469
- ...params: MaybeOptionalArg<${match(ctx.runtime)
470
- .with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
471
- .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
472
- .otherwise(() => `TEndpoint["parameters"]`)} & { withResponse: true; throwOnStatusError?: boolean }>
524
+ ...params: MaybeOptionalArg<
525
+ (TEndpoint extends { parameters: infer UParams }
526
+ ? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
527
+ : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean })
528
+ >
473
529
  ): Promise<SafeApiResponse<TEndpoint>>;
474
530
 
475
- ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
531
+ ${method}<Path extends keyof ${capitalizedMethod}Endpoints, _TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
476
532
  path: Path,
477
533
  ...params: MaybeOptionalArg<any>
478
534
  ): Promise<any> {
479
- const requestParams = params[0];
480
- const withResponse = requestParams?.withResponse;
481
- const { withResponse: _, throwOnStatusError = withResponse ? false : true, ...fetchParams } = requestParams || {};
482
-
483
- const promise = this.fetcher("${method}", this.baseUrl + path, Object.keys(fetchParams).length ? requestParams : undefined)
484
- .then(async (response) => {
485
- const data = await this.parseResponse(response);
486
- const typedResponse = Object.assign(response, {
487
- data: data,
488
- json: () => Promise.resolve(data)
489
- }) as SafeApiResponse<TEndpoint>;
490
-
491
- if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
492
- throw new TypedResponseError(typedResponse as never);
493
- }
494
-
495
- return withResponse ? typedResponse : data;
496
- });
497
-
498
- return promise ${match(ctx.runtime)
499
- .with("zod", "yup", () => `as Promise<${infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`)}>`)
500
- .with(
501
- "arktype",
502
- "io-ts",
503
- "typebox",
504
- "valibot",
505
- () => `as Promise<InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]>`,
506
- )
507
- .otherwise(() => `as Promise<Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>`)}
535
+ return this.request("${method}", path, ...params);
508
536
  }
509
537
  // </ApiClient.${method}>
510
538
  `
@@ -523,20 +551,74 @@ export class ApiClient {
523
551
  >(
524
552
  method: TMethod,
525
553
  path: TPath,
526
- ...params: MaybeOptionalArg<${match(ctx.runtime)
527
- .with("zod", "yup", () =>
528
- inferByRuntime[ctx.runtime](`TEndpoint extends { parameters: infer Params } ? Params : never`),
529
- )
530
- .with(
531
- "arktype",
532
- "io-ts",
533
- "typebox",
534
- "valibot",
535
- () => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`,
536
- )
537
- .otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
538
- : Promise<SafeApiResponse<TEndpoint>> {
539
- return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters) as Promise<SafeApiResponse<TEndpoint>>;
554
+ ...params: MaybeOptionalArg<
555
+ (TEndpoint extends { parameters: infer UParams }
556
+ ? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
557
+ : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean })
558
+ >
559
+ ): Promise<Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]>
560
+
561
+ request<
562
+ TMethod extends keyof EndpointByMethod,
563
+ TPath extends keyof EndpointByMethod[TMethod],
564
+ TEndpoint extends EndpointByMethod[TMethod][TPath]
565
+ >(
566
+ method: TMethod,
567
+ path: TPath,
568
+ ...params: MaybeOptionalArg<
569
+ (TEndpoint extends { parameters: infer UParams }
570
+ ? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
571
+ : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean })
572
+ >
573
+ ): Promise<SafeApiResponse<TEndpoint>>;
574
+
575
+ request<
576
+ TMethod extends keyof EndpointByMethod,
577
+ TPath extends keyof EndpointByMethod[TMethod],
578
+ TEndpoint extends EndpointByMethod[TMethod][TPath]
579
+ >(
580
+ method: TMethod,
581
+ path: TPath,
582
+ ...params: MaybeOptionalArg<any>
583
+ ): Promise<any> {
584
+ const requestParams = params[0];
585
+ const withResponse = requestParams?.withResponse;
586
+ const { withResponse: _, throwOnStatusError = withResponse ? false : true, overrides, ...fetchParams } = requestParams || {};
587
+
588
+ const parametersToSend: EndpointParameters = {};
589
+ if (requestParams?.body !== undefined) (parametersToSend as any).body = requestParams.body;
590
+ if (requestParams?.query !== undefined) (parametersToSend as any).query = requestParams.query;
591
+ if (requestParams?.header !== undefined) (parametersToSend as any).header = requestParams.header;
592
+ if (requestParams?.path !== undefined) (parametersToSend as any).path = requestParams.path;
593
+
594
+ const resolvedPath = (this.fetcher.decodePathParams ?? this.defaultDecodePathParams)(this.baseUrl + (path as string), (parametersToSend.path ?? {}) as Record<string, string>);
595
+ const url = new URL(resolvedPath);
596
+ const urlSearchParams = (this.fetcher.encodeSearchParams ?? this.defaultEncodeSearchParams)(parametersToSend.query);
597
+
598
+ const promise = this.fetcher.fetch({
599
+ method: method,
600
+ path: (path as string),
601
+ url,
602
+ urlSearchParams,
603
+ parameters: Object.keys(fetchParams).length ? fetchParams : undefined,
604
+ overrides,
605
+ throwOnStatusError
606
+ })
607
+ .then(async (response) => {
608
+ const data = await (this.fetcher.parseResponseData ?? this.defaultParseResponseData)(response);
609
+ const typedResponse = Object.assign(response, {
610
+ data: data,
611
+ json: () => Promise.resolve(data)
612
+ }) as SafeApiResponse<TEndpoint>;
613
+
614
+ if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
615
+ throw new TypedResponseError(typedResponse as never);
616
+ }
617
+
618
+ return withResponse ? typedResponse : data;
619
+ });
620
+
621
+ return promise as Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]
540
622
  }
541
623
  // </ApiClient.request>
542
624
  }
@@ -6,7 +6,14 @@ import { createBoxFactory } from "./box-factory.ts";
6
6
  import { openApiSchemaToTs } from "./openapi-schema-to-ts.ts";
7
7
  import { createRefResolver } from "./ref-resolver.ts";
8
8
  import { tsFactory } from "./ts-factory.ts";
9
- import { AnyBox, BoxRef, OpenapiSchemaConvertContext, type BoxObject, type LibSchemaObject } from "./types.ts";
9
+ import {
10
+ AnyBox,
11
+ BoxRef,
12
+ OpenapiSchemaConvertContext,
13
+ type BoxObject,
14
+ type BoxUnion,
15
+ type LibSchemaObject,
16
+ } from "./types.ts";
10
17
  import { pathToVariableName } from "./string-utils.ts";
11
18
  import { NameTransformOptions } from "./types.ts";
12
19
  import { match, P } from "ts-pattern";
@@ -134,7 +141,7 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
134
141
  }
135
142
 
136
143
  const allResponses: Record<string, AnyBox> = {};
137
- const allHeaders: Record<string, Box<BoxObject>> = {};
144
+ const allResponseHeaders: Record<string, Box<BoxObject>> = {};
138
145
 
139
146
  Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
140
147
  const responseObj = refs.unwrap<ResponseObject>(responseOrRef);
@@ -146,14 +153,26 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
146
153
  mediaTypes.forEach((mediaType) => {
147
154
  // If no JSON content, use unknown type
148
155
  const schema = content[mediaType] ? (content[mediaType].schema ?? {}) : {};
149
- const t = createBoxFactory(schema as LibSchemaObject, ctx);
150
156
  const mediaTypeResponse = openApiSchemaToTs({ schema, ctx });
151
157
 
152
158
  if (allResponses[status]) {
159
+ const t = createBoxFactory(
160
+ {
161
+ oneOf: [
162
+ ...((allResponses[status].schema as LibSchemaObject).oneOf
163
+ ? (allResponses[status].schema as LibSchemaObject).oneOf!
164
+ : [allResponses[status].schema]),
165
+ schema,
166
+ ],
167
+ } as LibSchemaObject,
168
+ ctx,
169
+ );
153
170
  allResponses[status] = t.union([
154
- ...(Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]]),
171
+ ...(allResponses[status].type === "union"
172
+ ? (allResponses[status] as Box<BoxUnion>).params.types
173
+ : [allResponses[status]]),
155
174
  mediaTypeResponse,
156
- ]);
175
+ ] as Box[]);
157
176
  } else {
158
177
  allResponses[status] = mediaTypeResponse;
159
178
  }
@@ -162,13 +181,25 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
162
181
  // If no content defined, use unknown type
163
182
  const schema = {};
164
183
  const unknown = openApiSchemaToTs({ schema: {}, ctx });
165
- const t = createBoxFactory(schema as LibSchemaObject, ctx);
166
184
 
167
185
  if (allResponses[status]) {
186
+ const t = createBoxFactory(
187
+ {
188
+ oneOf: [
189
+ ...((allResponses[status].schema as LibSchemaObject).oneOf
190
+ ? (allResponses[status].schema as LibSchemaObject).oneOf!
191
+ : [allResponses[status].schema]),
192
+ schema,
193
+ ],
194
+ } as LibSchemaObject,
195
+ ctx,
196
+ );
168
197
  allResponses[status] = t.union([
169
- ...(Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]]),
198
+ ...(allResponses[status].type === "union"
199
+ ? (allResponses[status] as Box<BoxUnion>).params.types
200
+ : [allResponses[status]]),
170
201
  unknown,
171
- ]);
202
+ ] as Box[]);
172
203
  } else {
173
204
  allResponses[status] = unknown;
174
205
  }
@@ -193,7 +224,7 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
193
224
  );
194
225
 
195
226
  if (Object.keys(mappedHeaders).length) {
196
- allHeaders[status] = t.object(mappedHeaders);
227
+ allResponseHeaders[status] = t.object(mappedHeaders);
197
228
  }
198
229
  }
199
230
  });
@@ -203,8 +234,8 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
203
234
  endpoint.responses = allResponses;
204
235
  }
205
236
 
206
- if (Object.keys(allHeaders).length) {
207
- endpoint.responseHeaders = allHeaders;
237
+ if (Object.keys(allResponseHeaders).length) {
238
+ endpoint.responseHeaders = allResponseHeaders;
208
239
  }
209
240
 
210
241
  endpointList.push(endpoint);
@@ -12,7 +12,7 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
12
12
 
13
13
  const file = `
14
14
  import { queryOptions } from "@tanstack/react-query"
15
- import type { EndpointByMethod, ApiClient, SuccessStatusCode, ErrorStatusCode, InferResponseByStatus } from "${ctx.relativeApiClientPath}"
15
+ import type { EndpointByMethod, ApiClient, SuccessStatusCode, ErrorStatusCode, InferResponseByStatus, TypedSuccessResponse } from "${ctx.relativeApiClientPath}"
16
16
  import { errorStatusCodes, TypedResponseError } from "${ctx.relativeApiClientPath}"
17
17
 
18
18
  type EndpointQueryKey<TOptions extends EndpointParameters> = [
@@ -66,6 +66,11 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
66
66
 
67
67
  type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
68
68
 
69
+ type InferResponseData<TEndpoint, TStatusCode> = TypedSuccessResponse<any, any, any> extends
70
+ InferResponseByStatus<TEndpoint, TStatusCode>
71
+ ? Extract<InferResponseByStatus<TEndpoint, TStatusCode>, { data: {}}>["data"]
72
+ : Extract<InferResponseByStatus<TEndpoint, TStatusCode>["data"], {}>;
73
+
69
74
  // </ApiClientTypes>
70
75
 
71
76
  // <ApiClient>
@@ -76,7 +81,10 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
76
81
  .map(
77
82
  (method) => `
78
83
  // <ApiClient.${method}>
79
- ${method}<Path extends keyof ${capitalize(method)}Endpoints, TEndpoint extends ${capitalize(method)}Endpoints[Path]>(
84
+ ${method}<
85
+ Path extends keyof ${capitalize(method)}Endpoints,
86
+ TEndpoint extends ${capitalize(method)}Endpoints[Path]
87
+ >(
80
88
  path: Path,
81
89
  ...params: MaybeOptionalArg<TEndpoint["parameters"]>
82
90
  ) {
@@ -91,28 +99,14 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
91
99
  const requestParams = {
92
100
  ...(params[0] || {}),
93
101
  ...(queryKey[0] || {}),
94
- signal,
102
+ overrides: { signal },
95
103
  withResponse: false as const
96
104
  };
97
- const res = await this.client.${method}(path, requestParams);
98
- return res as Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"];
105
+ const res = await this.client.${method}(path, requestParams as never);
106
+ return res as InferResponseData<TEndpoint, SuccessStatusCode>;
99
107
  },
100
108
  queryKey: queryKey
101
109
  }),
102
- mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook",
103
- mutationOptions: {
104
- mutationKey: queryKey,
105
- mutationFn: async (localOptions: TEndpoint extends { parameters: infer Parameters} ? Parameters: never) => {
106
- const requestParams = {
107
- ...(params[0] || {}),
108
- ...(queryKey[0] || {}),
109
- ...(localOptions || {}),
110
- withResponse: false as const
111
- };
112
- const res = await this.client.${method}(path, requestParams);
113
- return res as Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"];
114
- }
115
- }
116
110
  };
117
111
 
118
112
  return query
@@ -134,7 +128,7 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
134
128
  TWithResponse extends boolean = false,
135
129
  TSelection = TWithResponse extends true
136
130
  ? InferResponseByStatus<TEndpoint, SuccessStatusCode>
137
- : Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"],
131
+ : InferResponseData<TEndpoint, SuccessStatusCode>,
138
132
  TError = TEndpoint extends { responses: infer TResponses }
139
133
  ? TResponses extends Record<string | number, unknown>
140
134
  ? InferResponseByStatus<TEndpoint, ErrorStatusCode>
@@ -144,43 +138,57 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
144
138
  withResponse?: TWithResponse;
145
139
  selectFn?: (res: TWithResponse extends true
146
140
  ? InferResponseByStatus<TEndpoint, SuccessStatusCode>
147
- : Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]
141
+ : InferResponseData<TEndpoint, SuccessStatusCode>
148
142
  ) => TSelection;
149
143
  throwOnStatusError?: boolean
144
+ throwOnError?: boolean | ((error: TError) => boolean)
150
145
  }) {
151
146
  const mutationKey = [{ method, path }] as const;
147
+ const mutationFn = async <TLocalWithResponse extends boolean = TWithResponse, TLocalSelection = TLocalWithResponse extends true
148
+ ? InferResponseByStatus<TEndpoint, SuccessStatusCode>
149
+ : InferResponseData<TEndpoint, SuccessStatusCode>>
150
+ (params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
151
+ withResponse?: TLocalWithResponse;
152
+ throwOnStatusError?: boolean;
153
+ overrides?: RequestInit;
154
+ }): Promise<TLocalSelection> => {
155
+ const withResponse = params.withResponse ??options?.withResponse ?? false;
156
+ const throwOnStatusError = params.throwOnStatusError ?? options?.throwOnStatusError ?? (withResponse ? false : true);
157
+ const selectFn = options?.selectFn;
158
+ const response = await (this.client as any)[method](path, {
159
+ ...params as any,
160
+ withResponse: true,
161
+ throwOnStatusError: false,
162
+ });
163
+
164
+ if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
165
+ throw new TypedResponseError(response as never);
166
+ }
167
+
168
+ // Return just the data if withResponse is false, otherwise return the full response
169
+ const finalResponse = withResponse ? response : response.data;
170
+ const res = selectFn ? selectFn(finalResponse as any) : finalResponse;
171
+ return res as never;
172
+ };
152
173
  return {
153
174
  /** type-only property if you need easy access to the endpoint params */
154
175
  "~endpoint": {} as TEndpoint,
155
176
  mutationKey: mutationKey,
156
177
  mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook",
157
178
  mutationOptions: {
179
+ throwOnError: options?.throwOnError as boolean | ((error: TError) => boolean),
158
180
  mutationKey: mutationKey,
159
- mutationFn: async <TLocalWithResponse extends boolean = TWithResponse, TLocalSelection = TLocalWithResponse extends true
160
- ? InferResponseByStatus<TEndpoint, SuccessStatusCode>
161
- : Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>
162
- (params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
163
- withResponse?: TLocalWithResponse;
164
- throwOnStatusError?: boolean;
165
- }): Promise<TLocalSelection> => {
166
- const withResponse = params.withResponse ??options?.withResponse ?? false;
167
- const throwOnStatusError = params.throwOnStatusError ?? options?.throwOnStatusError ?? (withResponse ? false : true);
168
- const selectFn = options?.selectFn;
169
- const response = await (this.client as any)[method](path, { ...params as any, withResponse: true, throwOnStatusError: false });
170
-
171
- if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
172
- throw new TypedResponseError(response as never);
173
- }
174
-
175
- // Return just the data if withResponse is false, otherwise return the full response
176
- const finalResponse = withResponse ? response : response.data;
177
- const res = selectFn ? selectFn(finalResponse as any) : finalResponse;
178
- return res as never;
181
+ mutationFn: mutationFn,
182
+ } as Omit<import("@tanstack/react-query").UseMutationOptions<
183
+ TSelection,
184
+ TError,
185
+ (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
186
+ withResponse?: boolean;
187
+ throwOnStatusError?: boolean;
179
188
  }
180
- } satisfies import("@tanstack/react-query").UseMutationOptions<TSelection, TError, (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
181
- withResponse?: boolean;
182
- throwOnStatusError?: boolean;
183
- }>,
189
+ >, "mutationFn"> & {
190
+ mutationFn: typeof mutationFn
191
+ },
184
192
  }
185
193
  }
186
194
  // </ApiClient.request>