Token导航 LogoToken导航TokenDH.com
研究检索敏感数据github未标认证来源可访问许可证需确认审计异常

api-clientAPI client CLI

Agent Skill

用于辅助 API 设计、接口文档、请求响应结构和服务集成说明。它适合让 Agent 梳理 endpoint、生成 OpenAPI 草稿、检查字段命名、整理错误码或辅助前后端联调。使用时需要确认真实业务语义、鉴权方式、分页和错误处理规则;涉及生成接口文档时,应避免凭空补字段,最好从现有代码、schema 或接口样例中提取事实。

总安装

552

周安装

23

GitHub Stars

777

下载量

184
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:api-client(API client CLI)
来源仓库:https://github.com/dadbodgeoff/drift
仓库路径:skills/api-client
安装命令:
npx skills add https://github.com/dadbodgeoff/drift --skill api-client
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/dadbodgeoff/drift --skill api-client

简介

提供类型安全的 TypeScript API 客户端,集成自动令牌刷新与 TanStack Query 缓存。

  • 适用于前端应用调用后端服务,确保请求响应类型一致且减少重复逻辑。
  • 使用时需定义命名空间结构并注入中间件,支持统一错误处理和状态管理。
  • 安装方式:通过 npx skills add 从指定仓库添加,支持 Codex、Claude 等宿主环境。
  • 注意:组件层应封装具体 API 调用,保持与 UI 逻辑解耦以便复用和维护。

SKILL.md

TypeScript API Client

Centralized API client with typed namespaces, automatic token refresh, and TanStack Query integration.

When to Use This Skill

  • Building frontend applications that call backend APIs
  • Need type safety on requests and responses
  • Want automatic token refresh without duplicated logic
  • Using TanStack Query for caching and state management

Core Concepts

The pattern provides:

  • Typed namespaces (auth, users, billing, etc.)
  • Automatic token refresh with request deduplication
  • TanStack Query integration for caching
  • Consistent error handling with custom error class

Architecture:

Component → useQuery/useMutation → API Client → Fetch
                                       ↓
                                  401? → Refresh → Retry

Implementation

TypeScript

// lib/api/types.ts
export class APIClientError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number,
    public details?: Record<string, unknown>
  ) {
    super(message);
    this.name = 'APIClientError';
  }
}

export interface TokenPair {
  accessToken: string;
  refreshToken: string;
  expiresAt: string;
}

// lib/api/client.ts
interface RequestOptions {
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  body?: Record<string, unknown>;
  params?: Record<string, string | number | boolean | undefined>;
  skipRefresh?: boolean;
}

export class APIClient {
  private baseUrl: string;
  private accessToken: string | null = null;
  private refreshToken: string | null = null;
  private onUnauthorized: () => void;

  // Refresh deduplication
  private isRefreshing = false;
  private refreshPromise: Promise<boolean> | null = null;

  constructor(options: { baseUrl: string; onUnauthorized?: () => void }) {
    this.baseUrl = options.baseUrl.replace(/\/$/, '');
    this.onUnauthorized = options.onUnauthorized || (() => {});
  }

  setTokens(accessToken: string, refreshToken: string): void {
    this.accessToken = accessToken;
    this.refreshToken = refreshToken;
  }

  clearTokens(): void {
    this.accessToken = null;
    this.refreshToken = null;
  }

  // Typed namespaces
  auth = {
    login: (data: { email: string; password: string }) =>
      this.request<{ tokens: TokenPair; user: User }>('/auth/login', {
        method: 'POST',
        body: data,
      }),

    refresh: () =>
      this.request<TokenPair>('/auth/refresh', {
        method: 'POST',
        body: { refreshToken: this.refreshToken },
        skipRefresh: true, // Prevent infinite loop
      }),

    me: () =>
      this.request<User>('/auth/me', { method: 'GET' }),
  };

  users = {
    get: (id: string) =>
      this.request<User>(`/users/${id}`, { method: 'GET' }),

    update: (id: string, data: Partial<User>) =>
      this.request<User>(`/users/${id}`, { method: 'PATCH', body: data }),
  };

  private async request<T>(endpoint: string, options: RequestOptions): Promise<T> {
    const url = this.buildUrl(endpoint, options.params);

    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
    };

    if (this.accessToken) {
      headers['Authorization'] = `Bearer ${this.accessToken}`;
    }

    const response = await fetch(url, {
      method: options.method,
      headers,
      body: options.body ? JSON.stringify(options.body) : undefined,
    });

    // Handle 401 - attempt refresh
    if (response.status === 401 && !options.skipRefresh) {
      const refreshed = await this.attemptTokenRefresh();
      if (refreshed) {
        return this.request<T>(endpoint, { ...options, skipRefresh: true });
      }
      this.onUnauthorized();
      throw new APIClientError('Unauthorized', 'UNAUTHORIZED', 401);
    }

    if (!response.ok) {
      throw await this.parseError(response);
    }

    if (response.status === 204) return undefined as T;
    return this.transformResponse<T>(await response.json());
  }

  private async attemptTokenRefresh(): Promise<boolean> {
    if (!this.refreshToken) return false;

    // Deduplicate concurrent refresh attempts
    if (this.isRefreshing) {
      return this.refreshPromise!;
    }

    this.isRefreshing = true;
    this.refreshPromise = this.doRefresh();

    try {
      return await this.refreshPromise;
    } finally {
      this.isRefreshing = false;
      this.refreshPromise = null;
    }
  }

  private async doRefresh(): Promise<boolean> {
    try {
      const tokens = await this.auth.refresh();
      this.setTokens(tokens.accessToken, tokens.refreshToken);
      return true;
    } catch {
      this.clearTokens();
      return false;
    }
  }

  private buildUrl(endpoint: string, params?: Record<string, any>): string {
    const url = new URL(`${this.baseUrl}${endpoint}`);
    if (params) {
      Object.entries(params).forEach(([key, value]) => {
        if (value !== undefined) url.searchParams.set(key, String(value));
      });
    }
    return url.toString();
  }

  private transformResponse<T>(data: unknown): T {
    // Convert snake_case to camelCase
    return this.snakeToCamel(data) as T;
  }

  private snakeToCamel(obj: unknown): unknown {
    if (Array.isArray(obj)) return obj.map(item => this.snakeToCamel(item));
    if (obj !== null && typeof obj === 'object') {
      return Object.fromEntries(
        Object.entries(obj).map(([key, value]) => [
          key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()),
          this.snakeToCamel(value),
        ])
      );
    }
    return obj;
  }

  private async parseError(response: Response): Promise<APIClientError> {
    try {
      const data = await response.json();
      return new APIClientError(
        data.message || 'Request failed',
        data.code || 'UNKNOWN_ERROR',
        response.status,
        data.details
      );
    } catch {
      return new APIClientError('Request failed', 'UNKNOWN_ERROR', response.status);
    }
  }
}

// Singleton export
export const apiClient = new APIClient({
  baseUrl: process.env.NEXT_PUBLIC_API_URL || '/api',
  onUnauthorized: () => {
    if (typeof window !== 'undefined') window.location.href = '/login';
  },
});

TanStack Query Integration

// lib/api/query-keys.ts
export const queryKeys = {
  auth: {
    all: ['auth'] as const,
    me: () => [...queryKeys.auth.all, 'me'] as const,
  },
  users: {
    all: ['users'] as const,
    detail: (id: string) => [...queryKeys.users.all, id] as const,
  },
} as const;

// lib/api/hooks/use-auth.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

export function useCurrentUser() {
  return useQuery({
    queryKey: queryKeys.auth.me(),
    queryFn: () => apiClient.auth.me(),
    staleTime: 5 * 60 * 1000,
    retry: false,
  });
}

export function useLogin() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: { email: string; password: string }) =>
      apiClient.auth.login(data),
    onSuccess: (response) => {
      apiClient.setTokens(response.tokens.accessToken, response.tokens.refreshToken);
      queryClient.setQueryData(queryKeys.auth.me(), response.user);
    },
  });
}

Usage Examples

Component Usage

function UserProfile() {
  const { data: user, isLoading } = useCurrentUser();
  const logout = useLogout();

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h2>{user?.displayName}</h2>
      <button onClick={() => logout.mutate()}>Logout</button>
    </div>
  );
}

Best Practices

  1. Typed namespaces - Group related endpoints for discoverability
  2. Token refresh deduplication - Prevent multiple concurrent refresh requests
  3. Query key factory - Consistent cache key management
  4. Response transformation - Convert snake_case to camelCase automatically
  5. Singleton export - Single instance for consistent token state

Common Mistakes

  • Not deduplicating token refresh (causes race conditions)
  • Forgetting skipRefresh on refresh endpoint (infinite loop)
  • Scattered fetch calls without centralized error handling
  • No response transformation (inconsistent casing)
  • Creating multiple client instances (token state mismatch)

Related Patterns

  • jwt-auth - JWT authentication implementation
  • rate-limiting - Client-side rate limiting
  • error-handling - Error handling patterns

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

37.57%
按下载量换算69

Claude

30.38%
按下载量换算56

Cursor

19.9%
按下载量换算37

Gemini CLI

9.29%
按下载量换算17

安全审计

Gen Agent Trust Hub

未通过

Socket

通过

Snyk

通过

权限和风险

敏感数据

该 Skill 可能接触密钥、Token、环境变量或敏感配置,应进入高风险复核队列,默认不自动发布。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在 warning/failed 结果,不能写成本站确认安全。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills