Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Swagger TypeScript API

- Support for OpenAPI 3.0, 2.0, JSON and YAML
- Generate the API Client for Fetch or Axios from an OpenAPI Specification
- Generate the API Client for Fetch, Axios, or Ky from an OpenAPI Specification

Any questions you can ask here: <https://github.com/acacode/swagger-typescript-api/discussions>

Expand All @@ -19,6 +19,22 @@ You can use this package in two ways:
npx swagger-typescript-api generate --path ./swagger.json
```

#### Using different HTTP clients

Generate an API client with Ky:

```bash
npx swagger-typescript-api generate --path ./swagger.json --ky
```

Generate an API client with Axios:

```bash
npx swagger-typescript-api generate --path ./swagger.json --axios
```

By default, the Fetch API is used.

Or install locally in your project:

```bash
Expand All @@ -37,7 +53,20 @@ import * as path from "node:path";
import * as process from "node:process";
import { generateApi } from "swagger-typescript-api";

// Generate with default Fetch API
await generateApi({ input: path.resolve(process.cwd(), "./swagger.json") });

// Generate with Ky
await generateApi({
input: path.resolve(process.cwd(), "./swagger.json"),
httpClientType: "ky"
});

// Generate with Axios
await generateApi({
input: path.resolve(process.cwd(), "./swagger.json"),
httpClientType: "axios"
});
```

For more detailed configuration options, please consult the documentation.
Expand Down
9 changes: 8 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ const generateCommand = defineCommand({
description: "generate axios http client",
default: false,
},
ky: {
type: "boolean",
description: "generate ky http client",
default: false,
},
"clean-output": {
type: "boolean",
description:
Expand Down Expand Up @@ -314,7 +319,9 @@ const generateCommand = defineCommand({
httpClientType:
args["http-client"] || args.axios
? HTTP_CLIENT.AXIOS
: HTTP_CLIENT.FETCH,
: args.ky
? HTTP_CLIENT.KY
: HTTP_CLIENT.FETCH,
input: path.resolve(process.cwd(), args.path as string),
modular: args.modular,
moduleNameFirstTag: args["module-name-first-tag"],
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "swagger-typescript-api",
"version": "13.2.17",
"description": "Generate the API client for Fetch or Axios from an OpenAPI Specification",
"description": "Generate the API client for Fetch, Axios, or Ky from an OpenAPI Specification",
"homepage": "https://github.com/acacode/swagger-typescript-api",
"bugs": "https://github.com/acacode/swagger-typescript-api/issues",
"repository": "github:acacode/swagger-typescript-api",
Expand Down Expand Up @@ -67,6 +67,7 @@
"@types/node": "25.2.2",
"@types/swagger2openapi": "7.0.4",
"axios": "1.13.4",
"ky": "1.14.3",
"tsdown": "0.20.3",
"typedoc": "0.28.16",
"vitest": "4.0.18"
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const FILE_PREFIX = `/* eslint-disable */
export const HTTP_CLIENT = {
FETCH: "fetch",
AXIOS: "axios",
KY: "ky",
} as const;

export const PROJECT_VERSION = packageJson.version;
Expand Down
192 changes: 192 additions & 0 deletions templates/base/http-clients/ky-http-client.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<%
const { apiConfig, generateResponses, config } = it;
%>

import ky, { type Options as KyOptions, type KyResponse } from "ky";

export type QueryParamsType = Record<string | number, any>;

export interface FullRequestParams extends Omit<KyOptions, "json" | "method" | "searchParams"> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
path: string;
/** content type of request body */
type?: ContentType;
/** query params */
query?: QueryParamsType;
/** request body */
body?: unknown;
/** base url */
baseUrl?: string;
}

export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">

export interface ApiConfig<SecurityDataType = unknown> extends Omit<KyOptions, "json"> {
baseUrl?: string;
baseApiParams?: Omit<RequestParams, "baseUrl">;
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
}

export enum ContentType {
Json = "application/json",
JsonApi = "application/vnd.api+json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
Text = "text/plain",
}

export class HttpClient<SecurityDataType = unknown> {
public instance: typeof ky;
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private baseUrl: string;

private baseApiParams: Omit<RequestParams, "baseUrl"> = {
credentials: 'same-origin',
headers: {},
redirect: 'follow',
}

constructor({ baseUrl, baseApiParams, securityWorker, ...kyConfig }: ApiConfig<SecurityDataType> = {}) {
this.baseUrl = baseUrl || "<%~ apiConfig.baseUrl %>";
this.baseApiParams = { ...this.baseApiParams, ...baseApiParams };
this.securityWorker = securityWorker;

this.instance = ky.extend({
prefixUrl: this.baseUrl,
...kyConfig,
});
}

public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data;
}

protected encodeQueryParam(key: string, value: any) {
const encodedKey = encodeURIComponent(key);
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
}

protected addQueryParam(query: QueryParamsType, key: string) {
return this.encodeQueryParam(key, query[key]);
}

protected addArrayQueryParam(query: QueryParamsType, key: string) {
const value = query[key];
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
}

protected toQueryString(rawQuery?: QueryParamsType): string {
const query = rawQuery || {};
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
return keys
.map((key) =>
Array.isArray(query[key])
? this.addArrayQueryParam(query, key)
: this.addQueryParam(query, key),
)
.join("&");
}

protected addQueryParams(rawQuery?: QueryParamsType): string {
const queryString = this.toQueryString(rawQuery);
return queryString ? `?${queryString}` : "";
}

private contentFormatters: Record<ContentType, (input: any) => any> = {
[ContentType.Json]: (input: any) =>
input !== null && (typeof input === "object" || typeof input === "string") ? input : {},
[ContentType.JsonApi]: (input: any) =>
input !== null && (typeof input === "object" || typeof input === "string") ? input : {},
[ContentType.Text]: (input: any) => input !== null && typeof input !== "string" ? JSON.stringify(input) : input,
[ContentType.FormData]: (input: any) => Object.keys(input || {}).reduce((formData, key) => {
const property = input[key];
formData.append(
key,
property instanceof Blob || property instanceof File ? property : typeof property === "object" ? JSON.stringify(property) : `${property}`
);
return formData;
}, new FormData()),
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
};

protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
return {
...this.baseApiParams,
...params1,
...(params2 || {}),
headers: {
...(this.baseApiParams.headers || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
};
}

public request = async <T = any, E = any>({
secure,
path,
type,
query,
body,
baseUrl,
...params
<% if (config.unwrapResponseData) { %>
}: FullRequestParams): Promise<KyResponse<T>> => {
<% } else { %>
}: FullRequestParams): Promise<KyResponse<T>> => {
<% } %>
const secureParams = ((typeof secure === 'boolean' ? secure : false) && this.securityWorker && (await this.securityWorker(this.securityData))) || {};
const requestParams = this.mergeRequestParams(params, secureParams);

let requestBody = body;
let requestHeaders = requestParams.headers || {};

// Process body based on content type
if (type) {
if (type === ContentType.Json) {
requestBody = this.contentFormatters[ContentType.Json](body);
} else if (type === ContentType.FormData && body && body !== null && typeof body === "object") {
requestBody = this.contentFormatters[ContentType.FormData](body);
// Remove Content-Type header to let ky set it with proper boundary
delete requestHeaders["Content-Type"];
} else if (type === ContentType.UrlEncoded) {
requestBody = this.contentFormatters[ContentType.UrlEncoded](body);
requestHeaders = { ...requestHeaders, "Content-Type": type };
} else if (type === ContentType.Text) {
requestBody = this.contentFormatters[ContentType.Text](body);
requestHeaders = { ...requestHeaders, "Content-Type": type };
} else {
requestHeaders = { ...requestHeaders, "Content-Type": type };
}
}

// Build request options
const kyOptions: KyOptions = {
...requestParams,
headers: requestHeaders,
searchParams: query,
method: params.method || 'GET',
};

// Add body based on content type
if (requestBody !== undefined) {
if (type === ContentType.Json) {
kyOptions.json = requestBody;
} else {
kyOptions.body = requestBody;
}
}

// Prepare request URL
// When using prefixUrl (set in constructor), paths must not start with slash
// When using custom baseUrl, use full URL construction
const requestUrl = baseUrl
? new URL(path, baseUrl).toString()
: (path.startsWith('/') ? path.slice(1) : path);

return await this.instance<T>(requestUrl, kyOptions);
};
}
1 change: 1 addition & 0 deletions templates/default/api.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const descriptionLines = _.compact([
%>

<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %>
<% if (config.httpClientType === config.constants.HTTP_CLIENT.KY) { %> import type { Options as KyOptions, KyResponse } from "ky"; <% } %>

<% if (descriptionLines.length) { %>
/**
Expand Down
3 changes: 3 additions & 0 deletions templates/default/procedure-call.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ const describeReturnType = () => {
case HTTP_CLIENT.AXIOS: {
return `Promise<AxiosResponse<${type}>>`
}
case HTTP_CLIENT.KY: {
return `Promise<HttpResponse<${type}, ${errorType}>>`
}
default: {
return `Promise<HttpResponse<${type}, ${errorType}>>`
}
Expand Down
1 change: 1 addition & 0 deletions templates/modular/api.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const dataContracts = _.map(modelTypes, "name");
%>

<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %>
<% if (config.httpClientType === config.constants.HTTP_CLIENT.KY) { %> import type { Options as KyOptions, KyResponse } from "ky"; <% } %>

import { HttpClient, RequestParams, ContentType, HttpResponse } from "./<%~ config.fileNames.httpClient %>";
<% if (dataContracts.length) { %>
Expand Down
3 changes: 3 additions & 0 deletions templates/modular/procedure-call.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ const describeReturnType = () => {
case HTTP_CLIENT.AXIOS: {
return `Promise<AxiosResponse<${type}>>`
}
case HTTP_CLIENT.KY: {
return `Promise<HttpResponse<${type}, ${errorType}>>`
}
default: {
return `Promise<HttpResponse<${type}, ${errorType}>>`
}
Expand Down
Loading
Loading