typed-openapi 1.5.1 → 2.0.0

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.
@@ -1,5 +1,5 @@
1
1
  import * as arktype_internal_methods_object_ts from 'arktype/internal/methods/object.ts';
2
- import { N as NameTransformOptions } from './types-DLE5RaXi.js';
2
+ import { N as NameTransformOptions } from './types-DsI2d-HE.js';
3
3
  import 'openapi3-ts/oas31';
4
4
  import 'openapi3-ts/oas30';
5
5
 
@@ -8,6 +8,9 @@ declare const optionsSchema: arktype_internal_methods_object_ts.ObjectType<{
8
8
  tanstack: string | boolean;
9
9
  schemasOnly: boolean;
10
10
  output?: string;
11
+ includeClient?: boolean | "false" | "true";
12
+ successStatusCodes?: string;
13
+ errorStatusCodes?: string;
11
14
  }, {}>;
12
15
  type GenerateClientFilesOptions = typeof optionsSchema.infer & {
13
16
  nameTransform?: NameTransformOptions;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  generateClientFiles
3
- } from "./chunk-O7DZWQK4.js";
4
- import "./chunk-OVT6OLBK.js";
3
+ } from "./chunk-MCVYB63W.js";
4
+ import "./chunk-E6A7N4ND.js";
5
5
  import "./chunk-KAEXXJ7X.js";
6
6
  export {
7
7
  generateClientFiles
@@ -100,6 +100,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
100
100
  type DefaultEndpoint = {
101
101
  parameters?: EndpointParameters | undefined;
102
102
  response: AnyBox;
103
+ responses?: Record<string, AnyBox>;
103
104
  responseHeaders?: Record<string, AnyBox>;
104
105
  };
105
106
  type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
@@ -114,6 +115,7 @@ type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
114
115
  areParametersRequired: boolean;
115
116
  };
116
117
  response: TConfig["response"];
118
+ responses?: TConfig["responses"];
117
119
  responseHeaders?: TConfig["responseHeaders"];
118
120
  };
119
121
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "typed-openapi",
3
3
  "type": "module",
4
- "version": "1.5.1",
4
+ "version": "2.0.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "exports": {
@@ -30,8 +30,10 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@changesets/cli": "^2.29.4",
33
+ "@tanstack/react-query": "5.85.0",
33
34
  "@types/node": "^22.15.17",
34
35
  "@types/prettier": "3.0.0",
36
+ "msw": "2.10.5",
35
37
  "tsup": "^8.4.0",
36
38
  "typescript": "^5.8.3",
37
39
  "vitest": "^3.1.3"
@@ -64,6 +66,9 @@
64
66
  "dev": "tsup --watch",
65
67
  "build": "tsup",
66
68
  "test": "vitest",
69
+ "generate:runtime": "node bin.js ./tests/samples/petstore.yaml --output ./tmp/generated-client.ts --tanstack generated-tanstack.ts",
70
+ "test:runtime:run": "vitest run tests/integration-runtime-msw.test.ts",
71
+ "test:runtime": "pnpm run generate:runtime && pnpm run test:runtime:run",
67
72
  "fmt": "prettier --write src",
68
73
  "typecheck": "tsc -b ./tsconfig.build.json"
69
74
  }
package/src/cli.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { cac } from "cac";
2
-
3
2
  import { readFileSync } from "fs";
4
3
  import { generateClientFiles } from "./generate-client-files.ts";
5
4
  import { allowedRuntimes } from "./generator.ts";
@@ -11,16 +10,22 @@ cli
11
10
  .command("<input>", "Generate")
12
11
  .option("-o, --output <path>", "Output path for the api client ts file (defaults to `<input>.<runtime>.ts`)")
13
12
  .option(
14
- "-r, --runtime <name>",
13
+ "-r, --runtime <n>",
15
14
  `Runtime to use for validation; defaults to \`none\`; available: ${allowedRuntimes.toString()}`,
16
15
  { default: "none" },
17
16
  )
18
17
  .option("--schemas-only", "Only generate schemas, skipping client generation (defaults to false)", { default: false })
18
+ .option("--include-client", "Include API client types and implementation (defaults to true)", { default: true })
19
+ .option(
20
+ "--success-status-codes <codes>",
21
+ "Comma-separated list of success status codes (defaults to 2xx and 3xx ranges)",
22
+ )
23
+ .option("--error-status-codes <codes>", "Comma-separated list of error status codes (defaults to 4xx and 5xx ranges)")
19
24
  .option(
20
25
  "--tanstack [name]",
21
26
  "Generate tanstack client, defaults to false, can optionally specify a name for the generated file",
22
27
  )
23
- .action(async (input, _options) => {
28
+ .action(async (input: string, _options: any) => {
24
29
  return generateClientFiles(input, _options);
25
30
  });
26
31
 
@@ -3,7 +3,13 @@ import type { OpenAPIObject } from "openapi3-ts/oas31";
3
3
  import { basename, join, dirname } from "pathe";
4
4
  import { type } from "arktype";
5
5
  import { mkdir, writeFile } from "fs/promises";
6
- import { allowedRuntimes, generateFile } from "./generator.ts";
6
+ import {
7
+ allowedRuntimes,
8
+ generateFile,
9
+ DEFAULT_SUCCESS_STATUS_CODES,
10
+ DEFAULT_ERROR_STATUS_CODES,
11
+ type GeneratorOptions,
12
+ } from "./generator.ts";
7
13
  import { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
8
14
  import { generateTanstackQueryFile } from "./tanstack-query.generator.ts";
9
15
  import { prettify } from "./format.ts";
@@ -25,6 +31,9 @@ export const optionsSchema = type({
25
31
  runtime: allowedRuntimes,
26
32
  tanstack: "boolean | string",
27
33
  schemasOnly: "boolean",
34
+ "includeClient?": "boolean | 'true' | 'false'",
35
+ "successStatusCodes?": "string",
36
+ "errorStatusCodes?": "string",
28
37
  });
29
38
 
30
39
  type GenerateClientFilesOptions = typeof optionsSchema.infer & {
@@ -37,14 +46,31 @@ export async function generateClientFiles(input: string, options: GenerateClient
37
46
  const ctx = mapOpenApiEndpoints(openApiDoc, options);
38
47
  console.log(`Found ${ctx.endpointList.length} endpoints`);
39
48
 
40
- const content = await prettify(
41
- generateFile({
42
- ...ctx,
43
- runtime: options.runtime,
44
- schemasOnly: options.schemasOnly,
45
- nameTransform: options.nameTransform,
46
- }),
47
- );
49
+ // Parse success status codes if provided
50
+ const successStatusCodes = options.successStatusCodes
51
+ ? (options.successStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) as readonly number[])
52
+ : undefined;
53
+
54
+ // Parse error status codes if provided
55
+ const errorStatusCodes = options.errorStatusCodes
56
+ ? (options.errorStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) as readonly number[])
57
+ : undefined;
58
+
59
+ // Convert string boolean to actual boolean
60
+ const includeClient =
61
+ options.includeClient === "false" ? false : options.includeClient === "true" ? true : options.includeClient;
62
+
63
+ const generatorOptions: GeneratorOptions = {
64
+ ...ctx,
65
+ runtime: options.runtime,
66
+ schemasOnly: options.schemasOnly,
67
+ nameTransform: options.nameTransform,
68
+ includeClient: includeClient ?? true,
69
+ successStatusCodes: successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
70
+ errorStatusCodes: errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES,
71
+ };
72
+
73
+ const content = await prettify(generateFile(generatorOptions));
48
74
  const outputPath = join(
49
75
  cwd,
50
76
  options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`,
@@ -56,7 +82,7 @@ export async function generateClientFiles(input: string, options: GenerateClient
56
82
 
57
83
  if (options.tanstack) {
58
84
  const tanstackContent = await generateTanstackQueryFile({
59
- ...ctx,
85
+ ...generatorOptions,
60
86
  relativeApiClientPath: "./" + basename(outputPath),
61
87
  });
62
88
  const tanstackOutputPath = join(
package/src/generator.ts CHANGED
@@ -8,10 +8,26 @@ import { type } from "arktype";
8
8
  import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
9
9
  import type { NameTransformOptions } from "./types.ts";
10
10
 
11
- type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
11
+ // Default success status codes (2xx and 3xx ranges)
12
+ export const DEFAULT_SUCCESS_STATUS_CODES = [
13
+ 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308,
14
+ ] as const;
15
+
16
+ // Default error status codes (4xx and 5xx ranges)
17
+ export const DEFAULT_ERROR_STATUS_CODES = [
18
+ 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424,
19
+ 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511,
20
+ ] as const;
21
+
22
+ export type ErrorStatusCode = (typeof DEFAULT_ERROR_STATUS_CODES)[number];
23
+
24
+ export type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
12
25
  runtime?: "none" | keyof typeof runtimeValidationGenerator;
13
26
  schemasOnly?: boolean;
14
27
  nameTransform?: NameTransformOptions | undefined;
28
+ successStatusCodes?: readonly number[];
29
+ errorStatusCodes?: readonly number[];
30
+ includeClient?: boolean;
15
31
  };
16
32
  type GeneratorContext = Required<GeneratorOptions>;
17
33
 
@@ -62,11 +78,18 @@ const replacerByRuntime = {
62
78
  };
63
79
 
64
80
  export const generateFile = (options: GeneratorOptions) => {
65
- const ctx = { ...options, runtime: options.runtime ?? "none" } as GeneratorContext;
81
+ const ctx = {
82
+ ...options,
83
+ runtime: options.runtime ?? "none",
84
+ successStatusCodes: options.successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
85
+ errorStatusCodes: options.errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES,
86
+ includeClient: options.includeClient ?? true,
87
+ } as GeneratorContext;
66
88
 
67
89
  const schemaList = generateSchemaList(ctx);
68
90
  const endpointSchemaList = options.schemasOnly ? "" : generateEndpointSchemaList(ctx);
69
- const apiClient = options.schemasOnly ? "" : generateApiClient(ctx);
91
+ const endpointByMethod = options.schemasOnly ? "" : generateEndpointByMethod(ctx);
92
+ const apiClient = options.schemasOnly || !ctx.includeClient ? "" : generateApiClient(ctx);
70
93
 
71
94
  const transform =
72
95
  ctx.runtime === "none"
@@ -98,6 +121,7 @@ export const generateFile = (options: GeneratorOptions) => {
98
121
 
99
122
  const file = `
100
123
  ${transform(schemaList + endpointSchemaList)}
124
+ ${endpointByMethod}
101
125
  ${apiClient}
102
126
  `;
103
127
 
@@ -153,6 +177,24 @@ const responseHeadersObjectToString = (responseHeaders: Record<string, AnyBox>,
153
177
  return str + "}";
154
178
  };
155
179
 
180
+ const generateResponsesObject = (responses: Record<string, AnyBox>, ctx: GeneratorContext) => {
181
+ let str = "{";
182
+ for (const [statusCode, responseType] of Object.entries(responses)) {
183
+ const value =
184
+ ctx.runtime === "none"
185
+ ? responseType.recompute((box) => {
186
+ if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
187
+ box.value = `Schemas.${box.value}`;
188
+ }
189
+
190
+ return box;
191
+ }).value
192
+ : responseType.value;
193
+ str += `${wrapWithQuotesIfNeeded(statusCode)}: ${value},\n`;
194
+ }
195
+ return str + "}";
196
+ };
197
+
156
198
  const generateEndpointSchemaList = (ctx: GeneratorContext) => {
157
199
  let file = `
158
200
  ${ctx.runtime === "none" ? "export namespace Endpoints {" : ""}
@@ -199,6 +241,7 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
199
241
  }).value
200
242
  : endpoint.response.value
201
243
  },
244
+ ${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""}
202
245
  ${
203
246
  endpoint.responseHeaders
204
247
  ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders, ctx)},`
@@ -227,9 +270,11 @@ const generateEndpointByMethod = (ctx: GeneratorContext) => {
227
270
  ${Object.entries(byMethods)
228
271
  .map(([method, list]) => {
229
272
  return `${method}: {
230
- ${list.map(
231
- (endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`,
232
- )}
273
+ ${list
274
+ .map(
275
+ (endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`,
276
+ )
277
+ .join(",\n")}
233
278
  }`;
234
279
  })
235
280
  .join(",\n")}
@@ -251,9 +296,12 @@ const generateEndpointByMethod = (ctx: GeneratorContext) => {
251
296
  };
252
297
 
253
298
  const generateApiClient = (ctx: GeneratorContext) => {
299
+ if (!ctx.includeClient) {
300
+ return "";
301
+ }
302
+
254
303
  const { endpointList } = ctx;
255
304
  const byMethods = groupBy(endpointList, "method");
256
- const endpointSchemaList = generateEndpointByMethod(ctx);
257
305
 
258
306
  const apiClientTypes = `
259
307
  // <ApiClientTypes>
@@ -272,6 +320,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
272
320
  export type DefaultEndpoint = {
273
321
  parameters?: EndpointParameters | undefined;
274
322
  response: unknown;
323
+ responses?: Record<string, unknown>;
275
324
  responseHeaders?: Record<string, unknown>;
276
325
  };
277
326
 
@@ -287,11 +336,64 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
287
336
  areParametersRequired: boolean;
288
337
  };
289
338
  response: TConfig["response"];
339
+ responses?: TConfig["responses"];
290
340
  responseHeaders?: TConfig["responseHeaders"]
291
341
  };
292
342
 
293
343
  export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
294
344
 
345
+ export const successStatusCodes = [${ctx.successStatusCodes.join(",")}] as const;
346
+ export type SuccessStatusCode = typeof successStatusCodes[number];
347
+
348
+ export const errorStatusCodes = [${ctx.errorStatusCodes.join(",")}] as const;
349
+ export type ErrorStatusCode = typeof errorStatusCodes[number];
350
+
351
+ // Error handling types
352
+ /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
353
+ interface SuccessResponse<TSuccess, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
354
+ ok: true;
355
+ status: TStatusCode;
356
+ data: TSuccess;
357
+ /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
358
+ json: () => Promise<TSuccess>;
359
+ }
360
+
361
+ /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
362
+ interface ErrorResponse<TData, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
363
+ ok: false;
364
+ status: TStatusCode;
365
+ data: TData;
366
+ /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
367
+ json: () => Promise<TData>;
368
+ }
369
+
370
+ export type TypedApiResponse<TSuccess, TAllResponses extends Record<string | number, unknown> = {}> =
371
+ (keyof TAllResponses extends never
372
+ ? SuccessResponse<TSuccess, number>
373
+ : {
374
+ [K in keyof TAllResponses]: K extends string
375
+ ? K extends \`\${infer TStatusCode extends number}\`
376
+ ? TStatusCode extends SuccessStatusCode
377
+ ? SuccessResponse<TSuccess, TStatusCode>
378
+ : ErrorResponse<TAllResponses[K], TStatusCode>
379
+ : never
380
+ : K extends number
381
+ ? K extends SuccessStatusCode
382
+ ? SuccessResponse<TSuccess, K>
383
+ : ErrorResponse<TAllResponses[K], K>
384
+ : never;
385
+ }[keyof TAllResponses]);
386
+
387
+ export type SafeApiResponse<TEndpoint> = TEndpoint extends { response: infer TSuccess; responses: infer TResponses }
388
+ ? TResponses extends Record<string, unknown>
389
+ ? TypedApiResponse<TSuccess, TResponses>
390
+ : SuccessResponse<TSuccess, number>
391
+ : TEndpoint extends { response: infer TSuccess }
392
+ ? SuccessResponse<TSuccess, number>
393
+ : never;
394
+
395
+ export type InferResponseByStatus<TEndpoint, TStatusCode> = Extract<SafeApiResponse<TEndpoint>, { status: TStatusCode }>
396
+
295
397
  type RequiredKeys<T> = {
296
398
  [P in keyof T]-?: undefined extends T[P] ? never : P;
297
399
  }[keyof T];
@@ -302,9 +404,23 @@ type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [confi
302
404
  `;
303
405
 
304
406
  const apiClient = `
407
+ // <TypedResponseError>
408
+ export class TypedResponseError extends Error {
409
+ response: ErrorResponse<unknown, ErrorStatusCode>;
410
+ status: number;
411
+ constructor(response: ErrorResponse<unknown, ErrorStatusCode>) {
412
+ super(\`HTTP \${response.status}: \${response.statusText}\`);
413
+ this.name = 'TypedResponseError';
414
+ this.response = response;
415
+ this.status = response.status;
416
+ }
417
+ }
418
+ // </TypedResponseError>
305
419
  // <ApiClient>
306
420
  export class ApiClient {
307
421
  baseUrl: string = "";
422
+ successStatusCodes = successStatusCodes;
423
+ errorStatusCodes = errorStatusCodes;
308
424
 
309
425
  constructor(public fetcher: Fetcher) {}
310
426
 
@@ -333,16 +449,53 @@ export class ApiClient {
333
449
  ...params: MaybeOptionalArg<${match(ctx.runtime)
334
450
  .with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
335
451
  .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
336
- .otherwise(() => `TEndpoint["parameters"]`)}>
452
+ .otherwise(() => `TEndpoint["parameters"]`)} & { withResponse?: false; throwOnStatusError?: boolean }>
337
453
  ): Promise<${match(ctx.runtime)
338
454
  .with("zod", "yup", () => infer(`TEndpoint["response"]`))
339
455
  .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`)
340
- .otherwise(() => `TEndpoint["response"]`)}> {
341
- return this.fetcher("${method}", this.baseUrl + path, params[0])
342
- .then(response => this.parseResponse(response))${match(ctx.runtime)
456
+ .otherwise(() => `TEndpoint["response"]`)}>;
457
+
458
+ ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
459
+ path: Path,
460
+ ...params: MaybeOptionalArg<${match(ctx.runtime)
461
+ .with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
462
+ .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
463
+ .otherwise(() => `TEndpoint["parameters"]`)} & { withResponse: true; throwOnStatusError?: boolean }>
464
+ ): Promise<SafeApiResponse<TEndpoint>>;
465
+
466
+ ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
467
+ path: Path,
468
+ ...params: MaybeOptionalArg<any>
469
+ ): Promise<any> {
470
+ const requestParams = params[0];
471
+ const withResponse = requestParams?.withResponse;
472
+ const { withResponse: _, throwOnStatusError = withResponse ? false : true, ...fetchParams } = requestParams || {};
473
+
474
+ const promise = this.fetcher("${method}", this.baseUrl + path, Object.keys(fetchParams).length ? requestParams : undefined)
475
+ .then(async (response) => {
476
+ const data = await this.parseResponse(response);
477
+ const typedResponse = Object.assign(response, {
478
+ data: data,
479
+ json: () => Promise.resolve(data)
480
+ }) as SafeApiResponse<TEndpoint>;
481
+
482
+ if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
483
+ throw new TypedResponseError(typedResponse as never);
484
+ }
485
+
486
+ return withResponse ? typedResponse : data;
487
+ });
488
+
489
+ return promise ${match(ctx.runtime)
343
490
  .with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`)
344
- .with("arktype", "io-ts", "typebox", "valibot", () => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`)
345
- .otherwise(() => `as Promise<TEndpoint["response"]>`)};
491
+ .with(
492
+ "arktype",
493
+ "io-ts",
494
+ "typebox",
495
+ "valibot",
496
+ () => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`,
497
+ )
498
+ .otherwise(() => `as Promise<TEndpoint["response"]>`)}
346
499
  }
347
500
  // </ApiClient.${method}>
348
501
  `
@@ -373,11 +526,8 @@ export class ApiClient {
373
526
  () => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`,
374
527
  )
375
528
  .otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
376
- : Promise<Omit<Response, "json"> & {
377
- /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
378
- json: () => Promise<TEndpoint extends { response: infer Res } ? Res : never>;
379
- }> {
380
- return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
529
+ : Promise<SafeApiResponse<TEndpoint>> {
530
+ return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters) as Promise<SafeApiResponse<TEndpoint>>;
381
531
  }
382
532
  // </ApiClient.request>
383
533
  }
@@ -395,10 +545,25 @@ export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
395
545
  api.get("/users").then((users) => console.log(users));
396
546
  api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
397
547
  api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
548
+
549
+ // With error handling
550
+ const result = await api.get("/users/{id}", { path: { id: "123" }, withResponse: true });
551
+ if (result.ok) {
552
+ // Access data directly
553
+ const user = result.data;
554
+ console.log(user);
555
+
556
+ // Or use the json() method for compatibility
557
+ const userFromJson = await result.json();
558
+ console.log(userFromJson);
559
+ } else {
560
+ const error = result.data;
561
+ console.error(\`Error \${result.status}:\`, error);
562
+ }
398
563
  */
399
564
 
400
- // </ApiClient
565
+ // </ApiClient>
401
566
  `;
402
567
 
403
- return endpointSchemaList + apiClientTypes + apiClient;
568
+ return apiClientTypes + apiClient;
404
569
  };
@@ -130,14 +130,56 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
130
130
 
131
131
  // Match the first 2xx-3xx response found, or fallback to default one otherwise
132
132
  let responseObject: ResponseObject | undefined;
133
+ const allResponses: Record<string, AnyBox> = {};
134
+
133
135
  Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
134
136
  const statusCode = Number(status);
135
- if (statusCode >= 200 && statusCode < 300) {
136
- responseObject = refs.unwrap<ResponseObject>(responseOrRef);
137
+ const responseObj = refs.unwrap<ResponseObject>(responseOrRef);
138
+
139
+ // Collect all responses for error handling
140
+ const content = responseObj?.content;
141
+ if (content) {
142
+ const matchingMediaType = Object.keys(content).find(isResponseMediaType);
143
+ if (matchingMediaType && content[matchingMediaType]) {
144
+ allResponses[status] = openApiSchemaToTs({
145
+ schema: content[matchingMediaType]?.schema ?? {},
146
+ ctx,
147
+ });
148
+ } else {
149
+ // If no JSON content, use unknown type
150
+ allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
151
+ }
152
+ } else {
153
+ // If no content defined, use unknown type
154
+ allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
155
+ }
156
+
157
+ // Keep the current logic for the main response (first 2xx-3xx)
158
+ if (statusCode >= 200 && statusCode < 300 && !responseObject) {
159
+ responseObject = responseObj;
137
160
  }
138
161
  });
162
+
139
163
  if (!responseObject && operation.responses?.default) {
140
164
  responseObject = refs.unwrap(operation.responses.default);
165
+ // Also add default to all responses if not already covered
166
+ if (!allResponses["default"]) {
167
+ const content = responseObject?.content;
168
+ if (content) {
169
+ const matchingMediaType = Object.keys(content).find(isResponseMediaType);
170
+ if (matchingMediaType && content[matchingMediaType]) {
171
+ allResponses["default"] = openApiSchemaToTs({
172
+ schema: content[matchingMediaType]?.schema ?? {},
173
+ ctx,
174
+ });
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ // Set the responses collection
181
+ if (Object.keys(allResponses).length > 0) {
182
+ endpoint.responses = allResponses;
141
183
  }
142
184
 
143
185
  const content = responseObject?.content;
@@ -206,6 +248,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
206
248
  type DefaultEndpoint = {
207
249
  parameters?: EndpointParameters | undefined;
208
250
  response: AnyBox;
251
+ responses?: Record<string, AnyBox>;
209
252
  responseHeaders?: Record<string, AnyBox>;
210
253
  };
211
254
 
@@ -221,5 +264,6 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
221
264
  areParametersRequired: boolean;
222
265
  };
223
266
  response: TConfig["response"];
267
+ responses?: TConfig["responses"];
224
268
  responseHeaders?: TConfig["responseHeaders"];
225
269
  };