Install
$ agentstack add skill-showkkd133-api-client-gen-skill-api-client-gen Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Reads credentials/environment and may exfiltrate them.
What it can access
- ● Network access Used
- ● Filesystem access Used
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Skill: OpenAPI/Swagger TypeScript Client 生成
从 Swagger/OpenAPI 规范自动生成 TypeScript API 客户端,包括类型定义、fetch/axios 客户端、React Query hooks、SWR hooks。
触发条件
当用户请求以下操作时激活:
- "生成 api client"、"generate api client"、"从 swagger 生成客户端"
- "openapi client"、"swagger client"、"生成 ts 客户端"
- "从 openapi 生成类型"、"generate types from swagger"
执行步骤
第一步:获取 OpenAPI Spec
根据用户提供的输入方式获取 spec:
方式 A:URL
> ⚠️ 安全要求:下载前必须验证 URL > - 确认 URL 以 http:// 或 https:// 开头 > - 不接受 file://、ftp:// 等协议 > - 使用引号包裹 URL 防止 shell 注入
# 使用 mktemp 生成唯一临时文件,避免路径竞态
tmp_file=$(mktemp /tmp/openapi-spec-XXXXXX.json)
trap "rm -f $tmp_file" EXIT
# 直接下载 spec(带超时保护)
curl -s --max-time 30 --connect-timeout 10 "" -o "$tmp_file"
# 常见 swagger 端点
curl -s --max-time 30 --connect-timeout 10 "http://localhost:3000/api-docs" -o "$tmp_file"
curl -s --max-time 30 --connect-timeout 10 "http://localhost:3000/swagger.json" -o "$tmp_file"
curl -s --max-time 30 --connect-timeout 10 "http://localhost:8080/v3/api-docs" -o "$tmp_file" # Spring Boot
curl -s --max-time 30 --connect-timeout 10 "http://localhost:8000/openapi.json" -o "$tmp_file" # FastAPI
> 网络请求失败时: > 1. 检查 URL 是否可访问 > 2. 最多重试 2 次(间隔 3 秒) > 3. 仍失败则提示用户手动下载 spec 文件
方式 B:文件路径
# 查找项目中的 spec 文件(限制深度,排除无关目录)
find . -maxdepth 3 \( -name "openapi.*" -o -name "swagger.*" -o -name "api-docs.*" \) -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -10
ls docs/openapi.* docs/swagger.* 2>/dev/null
直接读取用户指定的文件路径(支持 .json、.yaml、.yml)。
方式 C:粘贴内容
用户直接粘贴 YAML/JSON 内容时,保存到临时文件后继续。
Spec 验证:
# 验证 spec 格式是否有效
bunx @apidevtools/swagger-cli validate "$tmp_file" 2>&1
验证失败时输出:
- spec 文件路径和大小
- 具体的验证错误信息
- 常见解决方案:
- OpenAPI 2.0 → 建议升级到 3.0(提供转换工具
swagger2openapi) - 缺少 required 字段 → 列出缺失的字段
$ref引用不存在 → 列出无效引用
- 允许用户修复后重新验证
第二步:解析 Spec 结构
Spec 结构预检查:
解析前先验证 spec 包含必要字段,缺失任一则中止并报告:
openapi(或swagger)— 版本号字段必须存在,确认是3.x或2.0info— 必须包含title和versionpaths— 必须存在且非空对象,否则无端点可生成
读取 spec 文件,提取以下关键信息:
- 基础信息 —
info.title、info.version、servers[].url - 认证方式 —
components.securitySchemes(Bearer / API Key / Cookie / OAuth2) - 所有端点 —
paths下的每个 path + method 组合 - Schema 定义 —
components.schemas下的所有类型 - 全局配置 — 公共参数、全局 security 要求
提取端点信息清单:
- HTTP method + path
- operationId(用于函数命名)
- parameters(path / query / header)
- requestBody schema
- responses(所有 status code + schema)
- tags(用于文件分组)
- security requirements
第三步:读取项目代码风格配置
# 检测项目配置
cat tsconfig.json 2>/dev/null | head -50
cat .eslintrc.json .eslintrc.js .eslintrc.yml 2>/dev/null | head -30
cat .prettierrc .prettierrc.json .prettierrc.js 2>/dev/null | head -20
cat biome.json 2>/dev/null | head -30
# 检测包管理器和现有依赖(先验证 JSON 格式有效)
if [ -f package.json ] && bun -e "JSON.parse(require('fs').readFileSync('package.json','utf8'))" 2>/dev/null; then
cat package.json | grep -E "(axios|@tanstack/react-query|swr|ky|got|ofetch)"
else
echo "⚠️ package.json 不存在或 JSON 格式无效,跳过依赖检测"
fi
适配规则:
| 项目配置 | 生成代码适配 | |---------|------------| | tsconfig.json 有 strict: true | 生成严格类型,不使用 any | | tsconfig.json 有 paths 别名 | import 使用对应别名 | | ESLint 使用 single quotes | 生成代码使用单引号 | | ESLint 禁止分号 | 生成代码不加分号 | | Prettier 配置 tabWidth: 4 | 使用 4 空格缩进 | | 项目已有 axios | 默认生成 axios 客户端 | | 项目已有 @tanstack/react-query | 自动生成 React Query hooks | | 项目已有 swr | 自动生成 SWR hooks |
默认代码风格(无配置时):
- 单引号、无分号、2 空格缩进
- 使用
const+ 箭头函数 - async/await 风格
第四步:生成 TypeScript 类型定义
OpenAPI → TypeScript 类型映射表
| OpenAPI Type | OpenAPI Format | TypeScript Type | |-------------|---------------|-----------------| | string | — | string | | string | date | string | | string | date-time | string | | string | email | string | | string | uri / url | string | | string | uuid | string | | string | binary | File \| Blob | | string | byte | string | | string + enum | — | 字面量联合类型 'a' \| 'b' \| 'c' | | number | — | number | | number | float / double | number | | integer | — | number | | integer | int32 / int64 | number | | boolean | — | boolean | | array | — | T[](T 为 items 类型) | | object | — | 生成 interface | | object + additionalProperties | — | Record | | $ref | — | 引用对应 interface 名称 | | oneOf | — | A \| B 联合类型 | | allOf | — | A & B 交叉类型 | | anyOf | — | A \| B 联合类型 | | nullable: true | — | T \| null |
类型生成规则
- Schema 名称 → interface 名称,PascalCase
- 属性名 → 保持 spec 原样(通常 camelCase)
- required 字段 → 非可选属性
- 非 required 字段 → 可选属性
prop?: Type - 枚举 → 导出为
type字面量联合 +const枚举数组 - 嵌套对象 → 提取为独立 interface
- 循环引用 → 使用 interface 名称引用(TypeScript 天然支持)
- deprecated 标注 → 当 spec 中操作或参数标注了
deprecated: true时,在生成的代码中添加@deprecatedJSDoc 注释,例如/** @deprecated 此接口即将废弃,请使用 xxx 替代 */
类型生成模板:
// --- types.ts ---
/** User entity */
export interface User {
id: string
name: string
email: string
role: UserRole
createdAt: string
avatar?: string | null
}
/** User role enum */
export type UserRole = 'admin' | 'user' | 'guest'
export const USER_ROLES = ['admin', 'user', 'guest'] as const
/** Create user request */
export interface CreateUserRequest {
name: string
email: string
role?: UserRole
}
/** Paginated response wrapper */
export interface PaginatedResponse {
data: T[]
total: number
page: number
limit: number
}
/** API error response */
export interface ApiError {
message: string
code: string
details?: Record
}
错误响应类型生成
从 spec 的 responses 中提取错误类型(4xx/5xx):
// 从 400 response schema 生成
export interface ValidationError {
message: string
field?: string
code?: string
}
// 从 401/403 response schema 生成
export interface AuthError {
message: string
code: 'UNAUTHORIZED' | 'FORBIDDEN'
}
// 通用错误类型
export interface ApiError {
status: number
message: string
details?: unknown
}
如果 spec 未定义错误 schema,生成通用的 ApiError 类型。
第五步:生成 API 客户端
询问用户偏好(或根据项目依赖自动检测):
- fetch(零依赖,默认)
- axios(如项目已安装 axios)
Fetch Client 模板
// --- client.ts ---
/** Fetch 客户端配置 */
export interface FetchClientConfig {
baseUrl: string
headers?: Record
/** Authentication token or token getter */
getToken?: () => string | Promise | null
/** API key for x-api-key header */
apiKey?: string
/** Request timeout in ms (default: 30000) */
timeout?: number
/** Retry count for failed requests (default: 0) */
retries?: number
/** Retry delay in ms (default: 1000) */
retryDelay?: number
/** Request/response interceptors */
onRequest?: (url: string, init: RequestInit) => RequestInit | Promise
onResponse?: (response: Response) => Response | Promise
onError?: (error: ApiClientError) => void
}
export class ApiClientError extends Error {
constructor(
message: string,
public readonly status: number,
public readonly body: unknown,
public readonly url: string,
public readonly method: string
) {
super(message)
this.name = 'ApiClientError'
}
}
const createClient = (config: FetchClientConfig) => {
const { baseUrl, timeout = 30000, retries = 0, retryDelay = 1000 } = config
const request = async (
method: string,
path: string,
options: {
params?: Record
body?: unknown
headers?: Record
} = {}
): Promise => {
// Build URL with query params — avoid new URL(path, base) which silently
// strips the base path (e.g. "https://api.example.com/v1" + "/users"
// would lose "/v1"). Use string concatenation instead.
const fullUrl = `${baseUrl.replace(/\/$/, '')}${path.startsWith('/') ? path : '/' + path}`
const url = new URL(fullUrl)
if (options.params) {
Object.entries(options.params).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.set(key, String(value))
}
})
}
// Detect if body is FormData (file upload)
const isFormData = options.body instanceof FormData
// Build headers — skip Content-Type for FormData (browser sets multipart boundary)
const headers: Record = {
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
...config.headers,
...options.headers,
}
if (isFormData) {
delete headers['Content-Type']
}
// Authentication
if (config.getToken) {
const token = await config.getToken()
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
}
if (config.apiKey) {
headers['X-API-Key'] = config.apiKey
}
// Serialize body — FormData is passed as-is, objects are JSON-stringified
const serializedBody = isFormData
? options.body as FormData
: options.body ? JSON.stringify(options.body) : undefined
let init: RequestInit = {
method,
headers,
body: serializedBody,
signal: AbortSignal.timeout(timeout),
}
// Request interceptor
if (config.onRequest) {
init = await config.onRequest(url.toString(), init)
}
// Retry logic
let lastError: Error | null = null
for (let attempt = 0; attempt null)
const error = new ApiClientError(
`${method} ${path} failed with status ${response.status}`,
response.status,
body,
url.toString(),
method
)
if (config.onError) {
config.onError(error)
}
throw error
}
// Handle 204 No Content
if (response.status === 204) {
return undefined as T
}
return (await response.json()) as T
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error))
// Only retry on network errors or 5xx, not on 4xx
const isRetryable =
!(error instanceof ApiClientError) ||
error.status >= 500
if (attempt setTimeout(resolve, retryDelay * (attempt + 1)))
continue
}
throw lastError
}
}
throw lastError
}
/** Request that returns a Blob (for file downloads) */
const requestBlob = async (
method: string,
path: string,
options: {
params?: Record
headers?: Record
} = {}
): Promise => {
const fullUrl = `${baseUrl.replace(/\/$/, '')}${path.startsWith('/') ? path : '/' + path}`
const url = new URL(fullUrl)
if (options.params) {
Object.entries(options.params).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.set(key, String(value))
}
})
}
const headers: Record = {
...config.headers,
...options.headers,
}
if (config.getToken) {
const token = await config.getToken()
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
}
if (config.apiKey) {
headers['X-API-Key'] = config.apiKey
}
let init: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(timeout),
}
if (config.onRequest) {
init = await config.onRequest(url.toString(), init)
}
const response = await fetch(url.toString(), init)
if (!response.ok) {
const body = await response.json().catch(() => null)
const error = new ApiClientError(
`${method} ${path} failed with status ${response.status}`,
response.status,
body,
url.toString(),
method
)
if (config.onError) {
config.onError(error)
}
throw error
}
return response.blob()
}
return { request, requestBlob }
}
Axios Client 模板
// --- client.ts (axios version) ---
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
/** Axios 客户端配置 */
export interface AxiosClientConfig {
baseUrl: string
headers?: Record
getToken?: () => string | Promise | null
apiKey?: string
timeout?: number
retries?: number
retryDelay?: number
}
export class ApiClientError extends Error {
constructor(
message: string,
public readonly status: number,
public readonly body: unknown,
public readonly url: string,
public readonly method: string
) {
super(message)
this.name = 'ApiClientError'
}
}
const createClient = (config: AxiosClientConfig): AxiosInstance => {
const { baseUrl, timeout = 30000, retries = 0, retryDelay = 1000 } = config
const instance = axios.create({
baseURL: baseUrl,
timeout,
headers: config.headers,
})
// Request interceptor: auth
instance.interceptors.request.use(async (reqConfig) => {
if (config.getToken) {
const token = await config.getToken()
if (token) {
reqConfig.headers.Authorization = `Bearer ${token}`
}
}
if (config.apiKey) {
reqConfig.headers['X-API-Key'] = config.apiKey
}
return reqConfig
})
// Response interceptor: retry + error transform
instance.interceptors.response.use(
(response) => response,
async (error) => {
const reqConfig = error.config as AxiosRequestConfig & { _retryCount?: number }
const retryCount = reqConfig._retryCount ?? 0
const status = error.response?.status ?? 0
if (retryCount = 500) {
reqConfig._retryCount = retryCount + 1
await new Promise((r) => setTimeout(r, retryDelay * (retryCount + 1)))
return instance.request(reqConfig)
}
throw new ApiClientError(
error.message,
status,
error.response?.data,
reqConfig.url ?? '',
reqConfig.method ?? ''
)
}
)
return instance
}
API 函数生成模板
对每个 operationId 生成一个函数:
// --- api/users.ts ---
import type { User, CreateUserRequest, PaginatedResponse } from '../types'
// operationId → function name (camelCase)
// tag → file grouping
/** List all users */
export const listUsers = (
client: ReturnType,
params?: { page?: number; limit?: number; search?: string }
): Promise> =>
client.request('GET', '/api/users', { params })
/** Get user by ID */
export const getUserById = (
client: ReturnType,
userId: string
): Promise =>
client.request('GET', `/api/users/${userId}`)
/** Create a new user */
export const createUser = (
client: ReturnType,
data: CreateUserRequest
): Promise =>
client.request('POST', '/api/users', { body: data })
/** Update user */
export const updateUser = (
client: ReturnType,
userId: string,
data: Partial
): Promise =>
client.request('PUT', `/api/users/${userId}`, { body: data })
/** Delete user */
export const deleteUser = (
client: ReturnType,
userId: string
): Promise =>
client.request('DELETE', `/api/users/${userId}`)
函数命名规则:
- 优先使用
operationId(转为 camelCase) - 若无 operationId,使用
{method}{PathSegments}格式:
GET /api/users→getApiUsersPOST /api/users/{id}/orders→postApiUsersOrders
- 路径参数(
{id}、{userId})从路径中去除,作为函数参数
第六步:生成 React Query Hooks(可选)
仅当项目已安装 @tanstack/react-query 或用户明确要求时生成。
hooks 生成规则:
GET→useQueryhookPOST/PUT/PATCH/DELETE→useMutationhook- 自动推断 query key 结构
- 支持
enabled、staleTime等配置透传
// --- hooks/useUsers.ts ---
import {
useQuery,
useMutation,
useQueryClient,
type UseQueryOptions,
type UseMutationOptions,
} from '@tanstack/react-query'
import type { User, CreateUserRequest, PaginatedResponse, ApiError } from '../types'
import type { createClient } from '../client'
import { listUsers, getUserById, createUser, updateUser, deleteUser } from '../api/users'
// ---- 客户端实例注入方式(二选一) ----
//
// 方式 A:模块级单例(简单项目推荐)
// 适合:单一 API 源、无需在组件树中动态切换客户端
// 调用 setApiClient(client) 初始化后即可使用所有 hooks
//
// 方式 B:React Context(多客户端或测试友好场景推荐)
// 适合:需要在不同组件子树使用不同客户端、便于测试时注入 mock
// 见下方独立的 Context 完整示例
//
// ---- 方式 A:模块级单例 ----
let _client: ReturnType
export const setApiClient = (clie
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [showkkd133](https://github.com/showkkd133)
- **Source:** [showkkd133/api-client-gen-skill](https://github.com/showkkd133/api-client-gen-skill)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.