AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL unreviewed MIT Self-run

Api Client Gen

skill-showkkd133-api-client-gen-skill-api-client-gen · by showkkd133

从 OpenAPI/Swagger 规范自动生成 TypeScript API 客户端(类型定义、fetch/axios 客户端、React Query/SWR hooks)

No reviews yet
0 installs
25 views
0.0% view→install

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

⚠ Flagged

1 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.

View the full security report →

Reliability & compatibility

Not yet reviewed
0 installs to date
no reviews yet
6mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

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 →
Are you the author of Api Client Gen? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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

验证失败时输出:

  1. spec 文件路径和大小
  2. 具体的验证错误信息
  3. 常见解决方案:
  • OpenAPI 2.0 → 建议升级到 3.0(提供转换工具 swagger2openapi
  • 缺少 required 字段 → 列出缺失的字段
  • $ref 引用不存在 → 列出无效引用
  1. 允许用户修复后重新验证

第二步:解析 Spec 结构

Spec 结构预检查:

解析前先验证 spec 包含必要字段,缺失任一则中止并报告:

  • openapi(或 swagger)— 版本号字段必须存在,确认是 3.x2.0
  • info — 必须包含 titleversion
  • paths — 必须存在且非空对象,否则无端点可生成

读取 spec 文件,提取以下关键信息:

  1. 基础信息info.titleinfo.versionservers[].url
  2. 认证方式components.securitySchemes(Bearer / API Key / Cookie / OAuth2)
  3. 所有端点paths 下的每个 path + method 组合
  4. Schema 定义components.schemas 下的所有类型
  5. 全局配置 — 公共参数、全局 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.jsonstrict: true | 生成严格类型,不使用 any | | tsconfig.jsonpaths 别名 | 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 |

类型生成规则
  1. Schema 名称 → interface 名称,PascalCase
  2. 属性名 → 保持 spec 原样(通常 camelCase)
  3. required 字段 → 非可选属性
  4. 非 required 字段 → 可选属性 prop?: Type
  5. 枚举 → 导出为 type 字面量联合 + const 枚举数组
  6. 嵌套对象 → 提取为独立 interface
  7. 循环引用 → 使用 interface 名称引用(TypeScript 天然支持)
  8. deprecated 标注 → 当 spec 中操作或参数标注了 deprecated: true 时,在生成的代码中添加 @deprecated JSDoc 注释,例如 /** @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}`)

函数命名规则:

  1. 优先使用 operationId(转为 camelCase)
  2. 若无 operationId,使用 {method}{PathSegments} 格式:
  • GET /api/usersgetApiUsers
  • POST /api/users/{id}/orderspostApiUsersOrders
  1. 路径参数({id}{userId})从路径中去除,作为函数参数

第六步:生成 React Query Hooks(可选)

仅当项目已安装 @tanstack/react-query 或用户明确要求时生成。

hooks 生成规则:

  • GETuseQuery hook
  • POST / PUT / PATCH / DELETEuseMutation hook
  • 自动推断 query key 结构
  • 支持 enabledstaleTime 等配置透传
// --- 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.

Versions

  • v0.1.0 Imported from the upstream source.