Claude-skill-registry api-client
Centralized TypeScript API client with typed namespaces, automatic token refresh with request deduplication, TanStack Query integration, and consistent error handling.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/api-client" ~/.claude/skills/majiayu000-claude-skill-registry-api-client-aab354 && rm -rf "$T"
manifest:
skills/data/api-client/SKILL.mdsafety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- references .env files
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
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
- Typed namespaces - Group related endpoints for discoverability
- Token refresh deduplication - Prevent multiple concurrent refresh requests
- Query key factory - Consistent cache key management
- Response transformation - Convert snake_case to camelCase automatically
- 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