typed-openapi
Version:
661 lines (574 loc) • 25.7 kB
text/typescript
import { capitalize, groupBy } from "pastable/server";
import { Box } from "./box.ts";
import { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
import { AnyBox, AnyBoxDef } from "./types.ts";
import * as Codegen from "@sinclair/typebox-codegen";
import { match } from "ts-pattern";
import { type } from "arktype";
import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
import type { BoxObject, NameTransformOptions } from "./types.ts";
// Default success status codes (2xx and 3xx ranges)
export const DEFAULT_SUCCESS_STATUS_CODES = [
200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308,
] as const;
// Default error status codes (4xx and 5xx ranges)
export const DEFAULT_ERROR_STATUS_CODES = [
400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424,
425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511,
] as const;
export type ErrorStatusCode = (typeof DEFAULT_ERROR_STATUS_CODES)[number];
export type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
runtime?: "none" | keyof typeof runtimeValidationGenerator;
schemasOnly?: boolean;
nameTransform?: NameTransformOptions | undefined;
successStatusCodes?: readonly number[];
errorStatusCodes?: readonly number[];
includeClient?: boolean;
};
type GeneratorContext = Required<GeneratorOptions>;
export const allowedRuntimes = type("'none' | 'arktype' | 'io-ts' | 'typebox' | 'valibot' | 'yup' | 'zod'");
export type OutputRuntime = typeof allowedRuntimes.infer;
// TODO validate response schemas in sample fetch ApiClient
// also, check that we can easily retrieve the response schema from the Fetcher
const runtimeValidationGenerator = {
arktype: Codegen.ModelToArkType.Generate,
"io-ts": Codegen.ModelToIoTs.Generate,
typebox: Codegen.ModelToTypeBox.Generate,
valibot: Codegen.ModelToValibot.Generate,
yup: Codegen.ModelToYup.Generate,
zod: Codegen.ModelToZod.Generate,
};
const inferByRuntime = {
none: (input: string) => input,
arktype: (input: string) => `${input}["infer"]`,
"io-ts": (input: string) => `t.TypeOf<${input}>`,
typebox: (input: string) => `Static<${input}>`,
valibot: (input: string) => `v.InferOutput<${input}>`,
yup: (input: string) => `y.InferType<${input}>`,
zod: (input: string) => `z.infer<${input}>`,
};
const methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"] as const;
const methodsRegex = new RegExp(`(?:${methods.join("|")})_`);
const endpointExport = new RegExp(`export (?:type|const) (?:${methodsRegex.source})`);
const replacerByRuntime = {
yup: (line: string) =>
line
.replace(/y\.InferType<\s*?typeof (.*?)\s*?>/g, "typeof $1")
.replace(
new RegExp(`(${endpointExport.source})` + new RegExp(/([\s\S]*? )(y\.object)(\()/).source, "g"),
"$1$2(",
),
zod: (line: string) =>
line
.replace(/z\.infer<\s*?typeof (.*?)\s*?>/g, "typeof $1")
.replace(
new RegExp(`(${endpointExport.source})` + new RegExp(/([\s\S]*? )(z\.object)(\()/).source, "g"),
"$1$2(",
),
};
export const generateFile = (options: GeneratorOptions) => {
const ctx = {
...options,
runtime: options.runtime ?? "none",
successStatusCodes: options.successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
errorStatusCodes: options.errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES,
includeClient: options.includeClient ?? true,
} as GeneratorContext;
const schemaList = generateSchemaList(ctx);
const endpointSchemaList = options.schemasOnly ? "" : generateEndpointSchemaList(ctx);
const endpointByMethod = options.schemasOnly ? "" : generateEndpointByMethod(ctx);
const apiClient = options.schemasOnly || !ctx.includeClient ? "" : generateApiClient(ctx);
const transform =
ctx.runtime === "none"
? (file: string) => file
: (file: string) => {
const model = Codegen.TypeScriptToModel.Generate(file);
const transformer = runtimeValidationGenerator[ctx.runtime as Exclude<typeof ctx.runtime, "none">];
// tmp fix for typebox, there's currently a "// todo" only with Codegen.ModelToTypeBox.Generate
// https://github.com/sinclairzx81/typebox-codegen/blob/44d44d55932371b69f349331b1c8a60f5d760d9e/src/model/model-to-typebox.ts#L31
const generated = ctx.runtime === "typebox" ? Codegen.TypeScriptToTypeBox.Generate(file) : transformer(model);
let converted = "";
const match = generated.match(/(const __ENDPOINTS_START__ =)([\s\S]*?)(export type __ENDPOINTS_END__)/);
const content = match?.[2];
if (content && ctx.runtime in replacerByRuntime) {
const before = generated.slice(0, generated.indexOf("export type __ENDPOINTS_START"));
converted =
before +
replacerByRuntime[ctx.runtime as keyof typeof replacerByRuntime](
content.slice(content.indexOf("export")),
);
} else {
converted = generated;
}
return converted;
};
const file = `
${transform(schemaList + endpointSchemaList)}
${endpointByMethod}
${apiClient}
`;
return file;
};
const generateSchemaList = ({ refs, runtime }: GeneratorContext) => {
let file = `
${runtime === "none" ? "export namespace Schemas {" : ""}
// <Schemas>
`;
refs.getOrderedSchemas().forEach(([schema, infos]) => {
if (!infos?.name) return;
if (infos.kind !== "schemas") return;
file += `export type ${infos.normalized} = ${schema.value}\n`;
});
return (
file +
`
// </Schemas>
${runtime === "none" ? "}" : ""}
`
);
};
const parameterObjectToString = (parameters: Box<AnyBoxDef> | Record<string, AnyBox>, ctx: GeneratorContext) => {
if (parameters instanceof Box) {
if (ctx.runtime === "none") {
return parameters.recompute((box) => {
if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
box.value = `Schemas.${box.value}`;
}
return box;
}).value;
}
return parameters.value;
}
let str = "{";
for (const [key, box] of Object.entries(parameters)) {
str += `${wrapWithQuotesIfNeeded(key)}${box.type === "optional" ? "?" : ""}: ${box.value},\n`;
}
return str + "}";
};
const responseHeadersObjectToString = (responseHeaders: Record<string, Box<BoxObject>>) => {
let str = "{";
for (const [key, responseHeader] of Object.entries(responseHeaders)) {
str += `${wrapWithQuotesIfNeeded(key.toLowerCase())}: ${responseHeader.value},\n`;
}
return str + "}";
};
const generateResponsesObject = (responses: Record<string, AnyBox>, ctx: GeneratorContext) => {
let str = "{";
for (const [statusCode, responseType] of Object.entries(responses)) {
const value =
ctx.runtime === "none"
? responseType.recompute((box) => {
if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
box.value = `Schemas.${box.value}`;
}
return box;
}).value
: responseType.value;
str += `${wrapWithQuotesIfNeeded(statusCode)}: ${value},\n`;
}
return str + "}";
};
const generateEndpointSchemaList = (ctx: GeneratorContext) => {
let file = `
${ctx.runtime === "none" ? "export namespace Endpoints {" : ""}
// <Endpoints>
${ctx.runtime === "none" ? "" : "type __ENDPOINTS_START__ = {}"}
`;
ctx.endpointList.map((endpoint) => {
const parameters = endpoint.parameters ?? {};
file += `export type ${endpoint.meta.alias} = {
method: "${endpoint.method.toUpperCase()}",
path: "${endpoint.path}",
requestFormat: "${endpoint.requestFormat}",
${
endpoint.meta.hasParameters
? `parameters: {
${parameters.query ? `query: ${parameterObjectToString(parameters.query, ctx)},` : ""}
${parameters.path ? `path: ${parameterObjectToString(parameters.path, ctx)},` : ""}
${parameters.header ? `header: ${parameterObjectToString(parameters.header, ctx)},` : ""}
${
parameters.body
? `body: ${parameterObjectToString(
ctx.runtime === "none"
? parameters.body.recompute((box) => {
if (Box.isReference(box) && !box.params.generics) {
box.value = `Schemas.${box.value}`;
}
return box;
})
: parameters.body,
ctx,
)},`
: ""
}
}`
: "parameters: never,"
}
${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""}
${endpoint.responseHeaders ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders)},` : ""}
}\n`;
});
return (
file +
`
// </Endpoints>
${ctx.runtime === "none" ? "}" : ""}
${ctx.runtime === "none" ? "" : "type __ENDPOINTS_END__ = {}"}
`
);
};
const generateEndpointByMethod = (ctx: GeneratorContext) => {
const { endpointList } = ctx;
const byMethods = groupBy(endpointList, "method");
const endpointByMethod = `
// <EndpointByMethod>
export ${ctx.runtime === "none" ? "type" : "const"} EndpointByMethod = {
${Object.entries(byMethods)
.map(([method, list]) => {
return `${method}: {
${list
.map(
(endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`,
)
.join(",\n")}
}`;
})
.join(",\n")}
}
${ctx.runtime === "none" ? "" : "export type EndpointByMethod = typeof EndpointByMethod;"}
// </EndpointByMethod>
`;
const shorthands = `
// <EndpointByMethod.Shorthands>
${Object.keys(byMethods)
.map((method) => `export type ${capitalize(method)}Endpoints = EndpointByMethod["${method}"]`)
.join("\n")}
// </EndpointByMethod.Shorthands>
`;
return endpointByMethod + shorthands;
};
const generateApiClient = (ctx: GeneratorContext) => {
if (!ctx.includeClient) {
return "";
}
const { endpointList } = ctx;
const byMethods = groupBy(endpointList, "method");
const apiClientTypes = `
// <ApiClientTypes>
export type EndpointParameters = {
body?: unknown;
query?: Record<string, unknown>;
header?: Record<string, unknown>;
path?: Record<string, unknown>;
};
export type MutationMethod = "post" | "put" | "patch" | "delete";
export type Method = "get" | "head" | "options" | MutationMethod;
type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
export type DefaultEndpoint = {
parameters?: EndpointParameters | undefined;
responses?: Record<string, unknown>;
responseHeaders?: Record<string, unknown>;
};
export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
operationId: string;
method: Method;
path: string;
requestFormat: RequestFormat;
parameters?: TConfig["parameters"];
meta: {
alias: string;
hasParameters: boolean;
areParametersRequired: boolean;
};
responses?: TConfig["responses"];
responseHeaders?: TConfig["responseHeaders"]
};
export interface Fetcher {
decodePathParams?: (path: string, pathParams: Record<string, string>) => string
encodeSearchParams?: (searchParams: Record<string, unknown> | undefined) => URLSearchParams
//
fetch: (input: {
method: Method;
url: URL;
urlSearchParams?: URLSearchParams | undefined;
parameters?: EndpointParameters | undefined;
path: string;
overrides?: RequestInit;
throwOnStatusError?: boolean
}) => Promise<Response>;
parseResponseData?: (response: Response) => Promise<unknown>
}
export const successStatusCodes = [${ctx.successStatusCodes.join(",")}] as const;
export type SuccessStatusCode = typeof successStatusCodes[number];
export const errorStatusCodes = [${ctx.errorStatusCodes.join(",")}] as const;
export type ErrorStatusCode = typeof errorStatusCodes[number];
// Taken from https://github.com/unjs/fetchdts/blob/ec4eaeab5d287116171fc1efd61f4a1ad34e4609/src/fetch.ts#L3
export interface TypedHeaders<TypedHeaderValues extends Record<string, string> | unknown> extends Omit<Headers, 'append' | 'delete' | 'get' | 'getSetCookie' | 'has' | 'set' | 'forEach'> {
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) */
append: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name, value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) => void
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) */
delete: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => void
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) */
get: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => (Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) | null
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) */
getSetCookie: () => string[]
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has) */
has: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => boolean
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) */
set: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name, value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) => void
forEach: (callbackfn: (value: TypedHeaderValues[keyof TypedHeaderValues] | string & {}, key: Extract<keyof TypedHeaderValues, string> | string & {}, parent: TypedHeaders<TypedHeaderValues>) => void, thisArg?: any) => void
}
/** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
export interface TypedSuccessResponse<TSuccess, TStatusCode, THeaders> extends Omit<Response, "ok" | "status" | "json" | "headers"> {
ok: true;
status: TStatusCode;
headers: never extends THeaders ? Headers : TypedHeaders<THeaders>;
data: TSuccess;
/** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
json: () => Promise<TSuccess>;
}
/** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
export interface TypedErrorResponse<TData, TStatusCode, THeaders> extends Omit<Response, "ok" | "status" | "json" | "headers"> {
ok: false;
status: TStatusCode;
headers: never extends THeaders ? Headers : TypedHeaders<THeaders>;
data: TData;
/** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
json: () => Promise<TData>;
}
export type TypedApiResponse<TAllResponses extends Record<string | number, unknown> = {}, THeaders = {}> =
({
[K in keyof TAllResponses]: K extends string
? K extends \`\${infer TStatusCode extends number}\`
? TStatusCode extends SuccessStatusCode
? TypedSuccessResponse<TAllResponses[K], TStatusCode, K extends keyof THeaders ? THeaders[K] : never>
: TypedErrorResponse<TAllResponses[K], TStatusCode, K extends keyof THeaders ? THeaders[K] : never>
: never
: K extends number
? K extends SuccessStatusCode
? TypedSuccessResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
: TypedErrorResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
: never;
}[keyof TAllResponses]);
export type SafeApiResponse<TEndpoint> = TEndpoint extends { responses: infer TResponses }
? TResponses extends Record<string, unknown>
? TypedApiResponse<TResponses, TEndpoint extends { responseHeaders: infer THeaders } ? THeaders : never>
: never
: never
export type InferResponseByStatus<TEndpoint, TStatusCode> = Extract<SafeApiResponse<TEndpoint>, { status: TStatusCode }>
type RequiredKeys<T> = {
[P in keyof T]-?: undefined extends T[P] ? never : P;
}[keyof T];
type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
type NotNever<T> = [T] extends [never] ? false : true;
// </ApiClientTypes>
`;
const infer = inferByRuntime[ctx.runtime];
const InferTEndpoint = match(ctx.runtime)
.with("zod", "yup", () => infer(`TEndpoint`))
.with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`))
.otherwise(() => `TEndpoint`);
const apiClient = `
// <TypedStatusError>
export class TypedStatusError<TData = unknown> extends Error {
response: TypedErrorResponse<TData, ErrorStatusCode, unknown>;
status: number;
constructor(response: TypedErrorResponse<TData, ErrorStatusCode, unknown>) {
super(\`HTTP \${response.status}: \${response.statusText}\`);
this.name = 'TypedStatusError';
this.response = response;
this.status = response.status;
}
}
// </TypedStatusError>
// <ApiClient>
export class ApiClient {
baseUrl: string = "";
successStatusCodes = successStatusCodes;
errorStatusCodes = errorStatusCodes;
constructor(public fetcher: Fetcher) {}
setBaseUrl(baseUrl: string) {
this.baseUrl = baseUrl;
return this;
}
/**
* Replace path parameters in URL
* Supports both OpenAPI format {param} and Express format :param
*/
defaultDecodePathParams = (url: string, params: Record<string, string>): string => {
return url
.replace(/{(\\w+)}/g, (_, key: string) => params[key] || \`{\${key}}\`)
.replace(/:([a-zA-Z0-9_]+)/g, (_, key: string) => params[key] || \`:\${key}\`);
}
/** Uses URLSearchParams, skips null/undefined values */
defaultEncodeSearchParams = (queryParams: Record<string, unknown> | undefined): URLSearchParams | undefined => {
if (!queryParams) return;
const searchParams = new URLSearchParams();
Object.entries(queryParams).forEach(([key, value]) => {
if (value != null) {
// Skip null/undefined values
if (Array.isArray(value)) {
value.forEach((val) => val != null && searchParams.append(key, String(val)));
} else {
searchParams.append(key, String(value));
}
}
});
return searchParams;
}
defaultParseResponseData = async (response: Response): Promise<unknown> => {
const contentType = response.headers.get("content-type") ?? "";
if (contentType.startsWith("text/")) {
return (await response.text())
}
if (contentType === "application/octet-stream") {
return (await response.arrayBuffer())
}
if (
contentType.includes("application/json") ||
(contentType.includes("application/") && contentType.includes("json")) ||
contentType === "*/*"
) {
try {
return await response.json();
} catch {
return undefined
}
}
return
}
${Object.entries(byMethods)
.map(([method, endpointByMethod]) => {
const capitalizedMethod = capitalize(method);
return endpointByMethod.length
? `// <ApiClient.${method}>
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
path: Path,
...params: MaybeOptionalArg<
(TEndpoint extends { parameters: infer UParams }
? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean })
>
): Promise<Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]>;
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
path: Path,
...params: MaybeOptionalArg<
(TEndpoint extends { parameters: infer UParams }
? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
: { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean })
>
): Promise<SafeApiResponse<TEndpoint>>;
${method}<Path extends keyof ${capitalizedMethod}Endpoints, _TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
path: Path,
...params: MaybeOptionalArg<any>
): Promise<any> {
return this.request("${method}", path, ...params);
}
// </ApiClient.${method}>
`
: "";
})
.join("\n")}
// <ApiClient.request>
/**
* Generic request method with full type-safety for any endpoint
*/
request<
TMethod extends keyof EndpointByMethod,
TPath extends keyof EndpointByMethod[TMethod],
TEndpoint extends EndpointByMethod[TMethod][TPath]
>(
method: TMethod,
path: TPath,
...params: MaybeOptionalArg<
(TEndpoint extends { parameters: infer UParams }
? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean })
>
): Promise<Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]>
request<
TMethod extends keyof EndpointByMethod,
TPath extends keyof EndpointByMethod[TMethod],
TEndpoint extends EndpointByMethod[TMethod][TPath]
>(
method: TMethod,
path: TPath,
...params: MaybeOptionalArg<
(TEndpoint extends { parameters: infer UParams }
? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
: { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean })
>
): Promise<SafeApiResponse<TEndpoint>>;
request<
TMethod extends keyof EndpointByMethod,
TPath extends keyof EndpointByMethod[TMethod],
TEndpoint extends EndpointByMethod[TMethod][TPath]
>(
method: TMethod,
path: TPath,
...params: MaybeOptionalArg<any>
): Promise<any> {
const requestParams = params[0];
const withResponse = requestParams?.withResponse;
const { withResponse: _, throwOnStatusError = withResponse ? false : true, overrides, ...fetchParams } = requestParams || {};
const parametersToSend: EndpointParameters = {};
if (requestParams?.body !== undefined) (parametersToSend as any).body = requestParams.body;
if (requestParams?.query !== undefined) (parametersToSend as any).query = requestParams.query;
if (requestParams?.header !== undefined) (parametersToSend as any).header = requestParams.header;
if (requestParams?.path !== undefined) (parametersToSend as any).path = requestParams.path;
const resolvedPath = (this.fetcher.decodePathParams ?? this.defaultDecodePathParams)(this.baseUrl + (path as string), (parametersToSend.path ?? {}) as Record<string, string>);
const url = new URL(resolvedPath);
const urlSearchParams = (this.fetcher.encodeSearchParams ?? this.defaultEncodeSearchParams)(parametersToSend.query);
const promise = this.fetcher.fetch({
method: method,
path: (path as string),
url,
urlSearchParams,
parameters: Object.keys(fetchParams).length ? fetchParams : undefined,
overrides,
throwOnStatusError
})
.then(async (response) => {
const data = await (this.fetcher.parseResponseData ?? this.defaultParseResponseData)(response);
const typedResponse = Object.assign(response, {
data: data,
json: () => Promise.resolve(data)
}) as SafeApiResponse<TEndpoint>;
if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
throw new TypedStatusError(typedResponse as never);
}
return withResponse ? typedResponse : data;
});
return promise as Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]
}
// </ApiClient.request>
}
export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
}
/**
Example usage:
const api = createApiClient((method, url, params) =>
fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
);
api.get("/users").then((users) => console.log(users));
api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
// With error handling
const result = await api.get("/users/{id}", { path: { id: "123" }, withResponse: true });
if (result.ok) {
// Access data directly
const user = result.data;
console.log(user);
// Or use the json() method for compatibility
const userFromJson = await result.json();
console.log(userFromJson);
} else {
const error = result.data;
console.error(\`Error \${result.status}:\`, error);
}
*/
// </ApiClient>
`;
return apiClientTypes + apiClient;
};