[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6EMBC8C1RAZJBQS1DKTKXSQ":3,"skill-same:01M6EMBC8C1RAZJBQS1DKTKXSQ":92},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":31,"publishedAt":52,"linkOnly":53,"markdownContent":54,"markdownBody":55,"frontMatter":56,"refName":57,"commitSha":58,"skillDirectory":59,"forksCount":60,"repositoryHtmlUrl":61,"repositoryHomepageUrl":62,"ownerHtmlUrl":63,"ownerBlogUrl":6,"ownerDescription":64,"updatedAt":65,"install":66,"resources":79},"01M6EMBC8C1RAZJBQS1DKTKXSQ","api-client",null,"Use when setting up API clients - TanStack Query, Axios, JWT token management, error handling, or response parsing.\nNOT when plain fetch calls, non-API data handling, or unrelated UI logic.\nTriggers: \"API client\", \"data fetching\", \"JWT token\", \"error handling\", \"paginated list\", \"TanStack Query\".","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",66,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fawais68\u002Fapi-client\u002FSKILL.md",[18,23,26],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":27,"categoryName":28,"taxonomyType":29,"taxonomyTypeDesc":30},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51],"api","client","api client","up","clients","error","triggers","logic","token","query","tanstack query","unrelated","error handling","use","fetch","list","tanstack","parsing","data","ui","2026-11-10 10:12:30",false,"---\nname: api-client\ndescription: |\n  Use when setting up API clients - TanStack Query, Axios, JWT token management, error handling, or response parsing.\n  NOT when plain fetch calls, non-API data handling, or unrelated UI logic.\n  Triggers: \"API client\", \"data fetching\", \"JWT token\", \"error handling\", \"paginated list\", \"TanStack Query\".\n---\n\n# API Client Skill\n\n## Overview\n\nExpert guidance for API client implementation using TanStack Query\u002FAxios, including JWT token attachment via interceptors, global error handling with toasts, type-safe response parsing with Zod, and offline detection for robust data fetching.\n\n## When This Skill Applies\n\nThis skill triggers when users request:\n- **API Setup**: \"Setup API client\", \"Configure TanStack Query\", \"Axios instance\"\n- **Data Fetching**: \"Fetch student data\", \"Get attendance\", \"API calls\"\n- **JWT\u002FToken**: \"Attach JWT token\", \"Bearer token headers\", \"Token refresh\"\n- **Error Handling**: \"API error toast\", \"Handle 401\", \"Retry failed requests\"\n- **Response Parsing**: \"Type-safe responses\", \"Zod validation\", \"Parse API data\"\n- **Pagination**: \"Paginated list\", \"Infinite query\", \"Load more data\"\n\n## Core Rules\n\n### 1. Setup: TanStack Query Configuration\n\n```typescript\n\u002F\u002F lib\u002FqueryClient.ts\nimport { QueryClient } from '@tanstack\u002Freact-query';\n\nexport const queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      staleTime: 5 * 60 * 1000, \u002F\u002F 5 minutes\n      gcTime: 10 * 60 * 1000, \u002F\u002F 10 minutes\n      retry: 3,\n      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),\n    },\n    mutations: {\n      retry: 1,\n    },\n  },\n});\n\n\u002F\u002F app\u002Flayout.tsx or app\u002Fproviders.tsx\n'use client';\nimport { QueryClientProvider } from '@tanstack\u002Freact-query';\nimport { queryClient } from '@\u002Flib\u002FqueryClient';\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  return (\n    \u003CQueryClientProvider client={queryClient}>\n      {children}\n    \u003C\u002FQueryClientProvider>\n  );\n}\n```\n\n**Requirements:**\n- Use TanStack Query v5 for data fetching\n- Configure appropriate staleTime and gcTime\n- Set retry strategy with exponential backoff\n- Wrap app with QueryClientProvider\n- Use Axios as fallback for complex scenarios\n\n### 2. JWT: Interceptors Auto-Attach\n\n```typescript\n\u002F\u002F lib\u002FapiClient.ts\nimport axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';\nimport { useAuthStore } from '@\u002Flib\u002Fauth-store';\n\nclass ApiClient {\n  private client: AxiosInstance;\n\n  constructor() {\n    this.client = axios.create({\n      baseURL: process.env.NEXT_PUBLIC_API_URL || 'http:\u002F\u002Flocalhost:3001\u002Fapi',\n      timeout: 10000, \u002F\u002F 10 seconds\n    });\n\n    this.setupInterceptors();\n  }\n\n  private setupInterceptors() {\n    \u002F\u002F Request interceptor - attach JWT token\n    this.client.interceptors.request.use(\n      (config: InternalAxiosRequestConfig) => {\n        const { session } = useAuthStore.getState();\n        if (session?.token && config.headers) {\n          config.headers.Authorization = `Bearer ${session.token}`;\n        }\n        return config;\n      },\n      (error) => Promise.reject(error)\n    );\n\n    \u002F\u002F Response interceptor - handle errors and 401\n    this.client.interceptors.response.use(\n      (response: AxiosResponse) => response,\n      async (error) => {\n        if (error.response?.status === 401) {\n          const { refresh } = useAuthStore.getState();\n          try {\n            const newToken = await refresh();\n            if (newToken) {\n              error.config!.headers!.Authorization = `Bearer ${newToken}`;\n              return this.client(error.config!);\n            }\n          } catch (refreshError) {\n            useAuthStore.getState().signOut();\n            window.location.href = '\u002Fauth\u002Flogin';\n          }\n        }\n        return Promise.reject(error);\n      }\n    );\n  }\n\n  get\u003CT>(url: string, config?: AxiosRequestConfig) {\n    return this.client.get\u003CT>(url, config);\n  }\n\n  post\u003CT>(url: string, data?: any, config?: AxiosRequestConfig) {\n    return this.client.post\u003CT>(url, data, config);\n  }\n\n  put\u003CT>(url: string, data?: any, config?: AxiosRequestConfig) {\n    return this.client.put\u003CT>(url, data, config);\n  }\n\n  delete\u003CT>(url: string, config?: AxiosRequestConfig) {\n    return this.client.delete\u003CT>(url, config);\n  }\n}\n\nexport const apiClient = new ApiClient();\n```\n\n**Requirements:**\n- Create Axios instance with baseURL and timeout\n- Request interceptor attaches JWT from auth store\n- Response interceptor handles 401 and token refresh\n- Automatic redirect to login on refresh failure\n- Type-safe methods with TypeScript generics\n\n### 3. Errors: Global Handler\n\n```typescript\n\u002F\u002F lib\u002FerrorHandler.ts\nimport axios from 'axios';\nimport { toast } from 'sonner';\n\nexport const handleApiError = (error: any) => {\n  if (axios.isAxiosError(error)) {\n    const message = error.response?.data?.message || error.message;\n\n    switch (error.response?.status) {\n      case 400:\n        toast.error('Bad Request', { description: message });\n        break;\n      case 401:\n        toast.error('Unauthorized', { description: 'Please log in again' });\n        break;\n      case 403:\n        toast.error('Forbidden', { description: 'You do not have permission' });\n        break;\n      case 404:\n        toast.error('Not Found', { description: message });\n        break;\n      case 429:\n        toast.error('Too Many Requests', { description: 'Please try again later' });\n        break;\n      case 500:\n        toast.error('Server Error', { description: message });\n        break;\n      default:\n        toast.error('Error', { description: message || 'Something went wrong' });\n    }\n  } else {\n    toast.error('Network Error', { description: error.message || 'Something went wrong' });\n  }\n};\n```\n\n```typescript\n\u002F\u002F hooks\u002FuseApi.ts\nimport { useQuery, useMutation, UseQueryOptions, UseMutationOptions } from '@tanstack\u002Freact-query';\nimport { apiClient } from '@\u002Flib\u002FapiClient';\nimport { handleApiError } from '@\u002Flib\u002FerrorHandler';\nimport { z } from 'zod';\n\nexport function useApi\u003CT>(\n  queryKey: any[],\n  url: string,\n  options?: Omit\u003CUseQueryOptions\u003CT>, 'queryKey' | 'queryFn'>\n) {\n  return useQuery({\n    queryKey,\n    queryFn: async () => {\n      const response = await apiClient.get\u003CT>(url);\n      return response.data;\n    },\n    ...options,\n  });\n}\n\nexport function useApiMutation\u003CT, V = any>(\n  url: string,\n  options?: Omit\u003CUseMutationOptions\u003CT, V, void>, 'mutationFn'>,\n  schema?: z.ZodSchema\u003CT>\n) {\n  return useMutation({\n    mutationFn: async (variables: V) => {\n      const response = await apiClient.post\u003CT>(url, variables);\n\n      \u002F\u002F Zod validation if schema provided\n      if (schema) {\n        try {\n          const parsed = schema.parse(response.data);\n          return parsed;\n        } catch (error) {\n          if (error instanceof z.ZodError) {\n            toast.error('Validation Error', { description: error.errors[0].message });\n            throw new Error(`Response validation failed: ${error.errors[0].message}`);\n          }\n        }\n      }\n\n      return response.data;\n    },\n    onError: (error) => {\n      options?.onError?.(error);\n      handleApiError(error);\n    },\n    onSuccess: (data, variables) => {\n      options?.onSuccess?.(data, variables);\n      if (options?.context?.successMessage) {\n        toast.success('Success', { description: options.context.successMessage });\n      }\n    },\n  });\n}\n```\n\n**Requirements:**\n- Global error handler with toast notifications\n- Handle all HTTP status codes appropriately\n- Zod schema validation for response parsing\n- Automatic error display in toasts\n- Success message handling for mutations\n\n### 4. Parsing: Typed Responses, Optimistic Updates\n\n```typescript\n\u002F\u002F lib\u002Fapi\u002Ftypes.ts\nimport { z } from 'zod';\n\n\u002F\u002F Student type with Zod schema\nexport const StudentSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  email: z.string().email(),\n  role: z.enum(['student', 'teacher', 'admin']),\n  classId: z.string().nullable(),\n  createdAt: z.string(),\n  updatedAt: z.string(),\n});\n\nexport type Student = z.infer\u003Ctypeof StudentSchema>;\n\n\u002F\u002F Attendance type\nexport const AttendanceSchema = z.object({\n  id: z.string(),\n  studentId: z.string(),\n  date: z.string(),\n  status: z.enum(['present', 'absent', 'late']),\n  notes: z.string().optional(),\n});\n\nexport type Attendance = z.infer\u003Ctypeof AttendanceSchema>;\n\n\u002F\u002F Paginated response type\nexport function PaginatedResponseSchema\u003CT extends z.ZodTypeAny>(itemSchema: T) {\n  return z.object({\n    data: z.array(itemSchema),\n    meta: z.object({\n      total: z.number(),\n      page: z.number(),\n      pageSize: z.number(),\n      totalPages: z.number(),\n    }),\n  });\n}\n\n\u002F\u002F hooks\u002FuseStudents.ts\nimport { useApi } from '.\u002FuseApi';\nimport { StudentSchema, PaginatedResponseSchema } from '@\u002Flib\u002Fapi\u002Ftypes';\n\nexport function useStudents(page = 1, pageSize = 20) {\n  return useApi(\n    ['students', 'page', page],\n    `\u002Fstudents?page=${page}&pageSize=${pageSize}`,\n    {\n      select: (data) => {\n        const parsed = PaginatedResponseSchema(StudentSchema).parse(data);\n        return parsed;\n      },\n    }\n  );\n}\n\n\u002F\u002F hooks\u002FuseUpdateStudent.ts\nexport function useUpdateStudent() {\n  const queryClient = useQueryClient();\n\n  return useApiMutation(\n    (variables: { id: string; data: Partial\u003CStudent> }) =>\n      `\u002Fstudents\u002F${variables.id}`,\n    {\n      onSuccess: (_, variables) => {\n        \u002F\u002F Invalidate and refetch\n        queryClient.invalidateQueries({ queryKey: ['students'] });\n        queryClient.invalidateQueries({ queryKey: ['student', variables.id] });\n      },\n      context: { successMessage: 'Student updated successfully' },\n    }\n  );\n}\n\n\u002F\u002F hooks\u002FuseDeleteStudent.ts\nexport function useDeleteStudent() {\n  const queryClient = useQueryClient();\n\n  return useApiMutation(\n    (id: string) => `\u002Fstudents\u002F${id}`,\n    {\n      onSuccess: () => {\n        queryClient.invalidateQueries({ queryKey: ['students'] });\n      },\n      context: { successMessage: 'Student deleted successfully' },\n    }\n  );\n}\n```\n\n```typescript\n\u002F\u002F Infinite queries for pagination\nimport { useInfiniteQuery } from '@tanstack\u002Freact-query';\nimport { StudentSchema } from '@\u002Flib\u002Fapi\u002Ftypes';\n\nexport function useInfiniteStudents() {\n  return useInfiniteQuery({\n    queryKey: ['students', 'infinite'],\n    queryFn: async ({ pageParam = 1 }) => {\n      const response = await apiClient.get(`\u002Fstudents?page=${pageParam}&pageSize=20`);\n      const data = response.data.map((item: any) => StudentSchema.parse(item));\n      return {\n        data,\n        nextPage: data.length === 20 ? pageParam + 1 : null,\n      };\n    },\n    initialPageParam: 1,\n    getNextPageParam: (lastPage) => lastPage.nextPage,\n  });\n}\n\n\u002F\u002F Optimistic updates with rollback\nexport function useUpdateAttendance() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async ({ studentId, date, status }: { studentId: string; date: string; status: string }) => {\n      return apiClient.put(`\u002Fattendance\u002F${studentId}\u002F${date}`, { status });\n    },\n    onMutate: async ({ studentId, date, status }) => {\n      \u002F\u002F Cancel outgoing queries\n      await queryClient.cancelQueries({ queryKey: ['attendance', studentId] });\n\n      \u002F\u002F Snapshot previous value\n      const previousAttendance = queryClient.getQueryData(['attendance', studentId]);\n\n      \u002F\u002F Optimistically update\n      queryClient.setQueryData(['attendance', studentId], (old: any) => ({\n        ...old,\n        data: old.data.map((item: any) =>\n          item.date === date ? { ...item, status } : item\n        ),\n      }));\n\n      return { previousAttendance };\n    },\n    onError: (error, variables, context) => {\n      \u002F\u002F Rollback on error\n      if (context?.previousAttendance) {\n        queryClient.setQueryData(['attendance', variables.studentId], context.previousAttendance);\n      }\n    },\n    onSettled: (_, __, variables) => {\n      \u002F\u002F Refetch on success or error\n      queryClient.invalidateQueries({ queryKey: ['attendance', variables.studentId] });\n    },\n  });\n}\n\n\u002F\u002F Offline detection\nexport function useOnlineStatus() {\n  const [isOnline, setIsOnline] = useState(navigator.onLine);\n\n  useEffect(() => {\n    const handleOnline = () => setIsOnline(true);\n    const handleOffline = () => setIsOnline(false);\n\n    window.addEventListener('online', handleOnline);\n    window.addEventListener('offline', handleOffline);\n\n    return () => {\n      window.removeEventListener('online', handleOnline);\n      window.removeEventListener('offline', handleOffline);\n    };\n  }, []);\n\n  return isOnline;\n}\n\n\u002F\u002F AbortController for cancelable requests\nexport function useFetchWithAbort\u003CT>(url: string) {\n  const [data, setData] = useState\u003CT | null>(null);\n  const [error, setError] = useState\u003CError | null>(null);\n  const [loading, setLoading] = useState(false);\n  const abortControllerRef = useRef\u003CAbortController | null>(null);\n\n  useEffect(() => {\n    return () => {\n      abortControllerRef.current?.abort();\n    };\n  }, []);\n\n  const fetchData = useCallback(async () => {\n    if (abortControllerRef.current) {\n      abortControllerRef.current.abort();\n    }\n\n    abortControllerRef.current = new AbortController();\n    setLoading(true);\n    setError(null);\n\n    try {\n      const response = await apiClient.get\u003CT>(url, {\n        signal: abortControllerRef.current.signal,\n      });\n      setData(response.data);\n    } catch (err) {\n      if (err instanceof Error && err.name !== 'AbortError') {\n        setError(err);\n      }\n    } finally {\n      setLoading(false);\n    }\n  }, [url]);\n\n  return { data, error, loading, refetch: fetchData, abort: () => abortControllerRef.current?.abort() };\n}\n```\n\n**Requirements:**\n- Infinite queries for paginated lists\n- Optimistic updates for immediate feedback\n- Rollback on error\n- Offline detection and handling\n- AbortController for cancelable requests\n\n## Output Requirements\n\n### Code Files\n\n1. **API Client**:\n   - `lib\u002FapiClient.ts` - Axios instance with interceptors\n   - `lib\u002FqueryClient.ts` - TanStack Query configuration\n\n2. **Error Handling**:\n   - `lib\u002FerrorHandler.ts` - Global error handler\n   - `hooks\u002FuseApi.ts` - Type-safe API hooks\n\n3. **Type Definitions**:\n   - `lib\u002Fapi\u002Ftypes.ts` - Zod schemas and types\n\n4. **Feature Hooks**:\n   - `hooks\u002FuseStudents.ts` - Student-specific hooks\n   - `hooks\u002FuseAttendance.ts` - Attendance-specific hooks\n\n### Integration Requirements\n\n- **@auth-integration**: Use JWT tokens from auth store\n- **@react-component**: Functional components with hooks\n- **@tailwind-css**: Responsive UI with mobile support\n\n### Documentation\n\n- **PHR**: Create Prompt History Record for API decisions\n- **ADR**: Document caching strategy, retry policy\n- **Comments**: Document API endpoints and data flow\n\n## Workflow\n\n1. **Setup API Client**\n   - Configure TanStack Query\n   - Create Axios instance\n   - Setup JWT interceptors\n\n2. **Define Types**\n   - Create Zod schemas\n   - Export TypeScript types\n\n3. **Create Hooks**\n   - Build useApi and useApiMutation\n   - Add feature-specific hooks\n   - Implement error handling\n\n4. **Integrate with Auth**\n   - Attach JWT tokens automatically\n   - Handle 401 responses\n   - Refresh tokens on expiry\n\n5. **Implement Features**\n   - Query hooks for data fetching\n   - Mutation hooks with optimistic updates\n   - Infinite queries for pagination\n\n6. **Test and Optimize**\n   - Test error scenarios\n   - Verify offline behavior\n   - Optimize caching strategy\n\n## Quality Checklist\n\nBefore completing any API client implementation:\n\n- [ ] **Typesafe Requests\u002FResponses**: Zod schemas for all data\n- [ ] **Retry on Fail**: Exponential backoff for retries\n- [ ] **Offline Detection**: Handle network disconnections\n- [ ] **AbortController**: Support cancelable requests\n- [ ] **JWT Auto-Attach**: Headers with Authorization Bearer\n- [ ] **Error Handling**: Global error handler with toasts\n- [ ] **401 Logout**: Automatic redirect on token expiry\n- [ ] **Zod Validation**: Response schema validation\n- [ ] **Optimistic Updates**: Immediate UI feedback\n- [ ] **Query Invalidation**: Automatic cache updates\n\n## Common Patterns\n\n### Fetch Student Data\n\n```typescript\n\u002F\u002F hooks\u002FuseStudent.ts\nexport function useStudent(id: string) {\n  return useApi(\n    ['student', id],\n    `\u002Fstudents\u002F${id}`,\n    {\n      enabled: !!id, \u002F\u002F Only fetch if id exists\n    }\n  );\n}\n\n\u002F\u002F Usage\nfunction StudentProfile({ studentId }: { studentId: string }) {\n  const { data: student, isLoading, error } = useStudent(studentId);\n\n  if (isLoading) return \u003CLoadingSkeleton \u002F>;\n  if (error) return \u003CErrorMessage error={error} \u002F>;\n\n  return (\n    \u003Cdiv>\n      \u003Ch1>{student?.name}\u003C\u002Fh1>\n      \u003Cp>{student?.email}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### API Error Toast with Zod Parse\n\n```typescript\n\u002F\u002F hooks\u002FuseCreateStudent.ts\nexport function useCreateStudent() {\n  const queryClient = useQueryClient();\n\n  return useApiMutation(\n    async (data: { name: string; email: string }) => {\n      const response = await apiClient.post('\u002Fstudents', data);\n      \u002F\u002F Zod validation\n      const parsed = StudentSchema.parse(response.data);\n      return parsed;\n    },\n    {\n      onSuccess: () => {\n        queryClient.invalidateQueries({ queryKey: ['students'] });\n      },\n      context: { successMessage: 'Student created successfully' },\n    }\n  );\n}\n\n\u002F\u002F Usage\nfunction CreateStudentForm() {\n  const { mutate: createStudent, isPending } = useCreateStudent();\n\n  const handleSubmit = (data: FormData) => {\n    createStudent(data);\n  };\n\n  return \u003Cform onSubmit={handleSubmit}>{\u002F* form fields *\u002F}\u003C\u002Fform>;\n}\n```\n\n### Paginated List with Infinite Query\n\n```typescript\n\u002F\u002F hooks\u002FuseInfiniteStudents.ts\nexport function useInfiniteStudents() {\n  return useInfiniteQuery({\n    queryKey: ['students', 'infinite'],\n    queryFn: async ({ pageParam = 1 }) => {\n      const response = await apiClient.get(`\u002Fstudents?page=${pageParam}&pageSize=20`);\n      const parsed = z.array(StudentSchema).parse(response.data);\n      return {\n        data: parsed,\n        nextPage: parsed.length === 20 ? pageParam + 1 : null,\n      };\n    },\n    initialPageParam: 1,\n    getNextPageParam: (lastPage) => lastPage.nextPage,\n  });\n}\n\n\u002F\u002F Usage\nfunction StudentList() {\n  const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteStudents();\n\n  return (\n    \u003Cdiv>\n      {data?.pages.map((page, i) => (\n        \u003Cdiv key={i}>\n          {page.data.map((student) => (\n            \u003CStudentCard key={student.id} student={student} \u002F>\n          ))}\n        \u003C\u002Fdiv>\n      ))}\n      {hasNextPage && (\n        \u003Cbutton\n          onClick={() => fetchNextPage()}\n          disabled={isFetchingNextPage}\n        >\n          {isFetchingNextPage ? 'Loading...' : 'Load More'}\n        \u003C\u002Fbutton>\n      )}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Attendance Fetch with Offline Support\n\n```typescript\n\u002F\u002F hooks\u002FuseAttendance.ts\nexport function useAttendance(studentId: string, date: string) {\n  const isOnline = useOnlineStatus();\n\n  return useApi(\n    ['attendance', studentId, date],\n    `\u002Fattendance\u002F${studentId}\u002F${date}`,\n    {\n      enabled: !!studentId && !!date && isOnline,\n      staleTime: 5 * 60 * 1000,\n    }\n  );\n}\n\n\u002F\u002F Usage\nfunction AttendanceCard({ studentId, date }: { studentId: string; date: string }) {\n  const { data: attendance, isLoading, error } = useAttendance(studentId, date);\n  const isOnline = useOnlineStatus();\n\n  if (!isOnline) {\n    return \u003COfflineMessage \u002F>;\n  }\n\n  if (isLoading) return \u003CLoadingSkeleton \u002F>;\n  if (error) return \u003CErrorMessage error={error} \u002F>;\n\n  return (\n    \u003Cdiv>\n      \u003Cp>Status: {attendance?.status}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## Caching Strategy\n\n```typescript\n\u002F\u002F lib\u002FqueryClient.ts\nexport const queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      \u002F\u002F Fresh data is considered stale after 5 minutes\n      staleTime: 5 * 60 * 1000,\n      \u002F\u002F Garbage collect unused queries after 10 minutes\n      gcTime: 10 * 60 * 1000,\n      \u002F\u002F Retry failed requests 3 times\n      retry: 3,\n      \u002F\u002F Exponential backoff: 1s, 2s, 4s (max 30s)\n      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),\n      \u002F\u002F Refetch on window focus (optional)\n      refetchOnWindowFocus: false,\n      \u002F\u002F Refetch on reconnect\n      refetchOnReconnect: true,\n    },\n  },\n});\n```\n\n## Environment Variables\n\n```bash\n# .env.local\nNEXT_PUBLIC_API_URL=http:\u002F\u002Flocalhost:3001\u002Fapi\n# For production\nNEXT_PUBLIC_API_URL=https:\u002F\u002Fapi.yourapp.com\n```\n\n## References\n\n- TanStack Query: https:\u002F\u002Ftanstack.com\u002Fquery\u002Flatest\n- Axios: https:\u002F\u002Faxios-http.com\n- Zod: https:\u002F\u002Fzod.dev\n- React Query Examples: https:\u002F\u002Ftanstack.com\u002Fquery\u002Flatest\u002Fdocs\u002Freact\u002Fexamples\n","# API Client Skill\n\n## Overview\n\nExpert guidance for API client implementation using TanStack Query\u002FAxios, including JWT token attachment via interceptors, global error handling with toasts, type-safe response parsing with Zod, and offline detection for robust data fetching.\n\n## When This Skill Applies\n\nThis skill triggers when users request:\n- **API Setup**: \"Setup API client\", \"Configure TanStack Query\", \"Axios instance\"\n- **Data Fetching**: \"Fetch student data\", \"Get attendance\", \"API calls\"\n- **JWT\u002FToken**: \"Attach JWT token\", \"Bearer token headers\", \"Token refresh\"\n- **Error Handling**: \"API error toast\", \"Handle 401\", \"Retry failed requests\"\n- **Response Parsing**: \"Type-safe responses\", \"Zod validation\", \"Parse API data\"\n- **Pagination**: \"Paginated list\", \"Infinite query\", \"Load more data\"\n\n## Core Rules\n\n### 1. Setup: TanStack Query Configuration\n\n```typescript\n\u002F\u002F lib\u002FqueryClient.ts\nimport { QueryClient } from '@tanstack\u002Freact-query';\n\nexport const queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      staleTime: 5 * 60 * 1000, \u002F\u002F 5 minutes\n      gcTime: 10 * 60 * 1000, \u002F\u002F 10 minutes\n      retry: 3,\n      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),\n    },\n    mutations: {\n      retry: 1,\n    },\n  },\n});\n\n\u002F\u002F app\u002Flayout.tsx or app\u002Fproviders.tsx\n'use client';\nimport { QueryClientProvider } from '@tanstack\u002Freact-query';\nimport { queryClient } from '@\u002Flib\u002FqueryClient';\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  return (\n    \u003CQueryClientProvider client={queryClient}>\n      {children}\n    \u003C\u002FQueryClientProvider>\n  );\n}\n```\n\n**Requirements:**\n- Use TanStack Query v5 for data fetching\n- Configure appropriate staleTime and gcTime\n- Set retry strategy with exponential backoff\n- Wrap app with QueryClientProvider\n- Use Axios as fallback for complex scenarios\n\n### 2. JWT: Interceptors Auto-Attach\n\n```typescript\n\u002F\u002F lib\u002FapiClient.ts\nimport axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';\nimport { useAuthStore } from '@\u002Flib\u002Fauth-store';\n\nclass ApiClient {\n  private client: AxiosInstance;\n\n  constructor() {\n    this.client = axios.create({\n      baseURL: process.env.NEXT_PUBLIC_API_URL || 'http:\u002F\u002Flocalhost:3001\u002Fapi',\n      timeout: 10000, \u002F\u002F 10 seconds\n    });\n\n    this.setupInterceptors();\n  }\n\n  private setupInterceptors() {\n    \u002F\u002F Request interceptor - attach JWT token\n    this.client.interceptors.request.use(\n      (config: InternalAxiosRequestConfig) => {\n        const { session } = useAuthStore.getState();\n        if (session?.token && config.headers) {\n          config.headers.Authorization = `Bearer ${session.token}`;\n        }\n        return config;\n      },\n      (error) => Promise.reject(error)\n    );\n\n    \u002F\u002F Response interceptor - handle errors and 401\n    this.client.interceptors.response.use(\n      (response: AxiosResponse) => response,\n      async (error) => {\n        if (error.response?.status === 401) {\n          const { refresh } = useAuthStore.getState();\n          try {\n            const newToken = await refresh();\n            if (newToken) {\n              error.config!.headers!.Authorization = `Bearer ${newToken}`;\n              return this.client(error.config!);\n            }\n          } catch (refreshError) {\n            useAuthStore.getState().signOut();\n            window.location.href = '\u002Fauth\u002Flogin';\n          }\n        }\n        return Promise.reject(error);\n      }\n    );\n  }\n\n  get\u003CT>(url: string, config?: AxiosRequestConfig) {\n    return this.client.get\u003CT>(url, config);\n  }\n\n  post\u003CT>(url: string, data?: any, config?: AxiosRequestConfig) {\n    return this.client.post\u003CT>(url, data, config);\n  }\n\n  put\u003CT>(url: string, data?: any, config?: AxiosRequestConfig) {\n    return this.client.put\u003CT>(url, data, config);\n  }\n\n  delete\u003CT>(url: string, config?: AxiosRequestConfig) {\n    return this.client.delete\u003CT>(url, config);\n  }\n}\n\nexport const apiClient = new ApiClient();\n```\n\n**Requirements:**\n- Create Axios instance with baseURL and timeout\n- Request interceptor attaches JWT from auth store\n- Response interceptor handles 401 and token refresh\n- Automatic redirect to login on refresh failure\n- Type-safe methods with TypeScript generics\n\n### 3. Errors: Global Handler\n\n```typescript\n\u002F\u002F lib\u002FerrorHandler.ts\nimport axios from 'axios';\nimport { toast } from 'sonner';\n\nexport const handleApiError = (error: any) => {\n  if (axios.isAxiosError(error)) {\n    const message = error.response?.data?.message || error.message;\n\n    switch (error.response?.status) {\n      case 400:\n        toast.error('Bad Request', { description: message });\n        break;\n      case 401:\n        toast.error('Unauthorized', { description: 'Please log in again' });\n        break;\n      case 403:\n        toast.error('Forbidden', { description: 'You do not have permission' });\n        break;\n      case 404:\n        toast.error('Not Found', { description: message });\n        break;\n      case 429:\n        toast.error('Too Many Requests', { description: 'Please try again later' });\n        break;\n      case 500:\n        toast.error('Server Error', { description: message });\n        break;\n      default:\n        toast.error('Error', { description: message || 'Something went wrong' });\n    }\n  } else {\n    toast.error('Network Error', { description: error.message || 'Something went wrong' });\n  }\n};\n```\n\n```typescript\n\u002F\u002F hooks\u002FuseApi.ts\nimport { useQuery, useMutation, UseQueryOptions, UseMutationOptions } from '@tanstack\u002Freact-query';\nimport { apiClient } from '@\u002Flib\u002FapiClient';\nimport { handleApiError } from '@\u002Flib\u002FerrorHandler';\nimport { z } from 'zod';\n\nexport function useApi\u003CT>(\n  queryKey: any[],\n  url: string,\n  options?: Omit\u003CUseQueryOptions\u003CT>, 'queryKey' | 'queryFn'>\n) {\n  return useQuery({\n    queryKey,\n    queryFn: async () => {\n      const response = await apiClient.get\u003CT>(url);\n      return response.data;\n    },\n    ...options,\n  });\n}\n\nexport function useApiMutation\u003CT, V = any>(\n  url: string,\n  options?: Omit\u003CUseMutationOptions\u003CT, V, void>, 'mutationFn'>,\n  schema?: z.ZodSchema\u003CT>\n) {\n  return useMutation({\n    mutationFn: async (variables: V) => {\n      const response = await apiClient.post\u003CT>(url, variables);\n\n      \u002F\u002F Zod validation if schema provided\n      if (schema) {\n        try {\n          const parsed = schema.parse(response.data);\n          return parsed;\n        } catch (error) {\n          if (error instanceof z.ZodError) {\n            toast.error('Validation Error', { description: error.errors[0].message });\n            throw new Error(`Response validation failed: ${error.errors[0].message}`);\n          }\n        }\n      }\n\n      return response.data;\n    },\n    onError: (error) => {\n      options?.onError?.(error);\n      handleApiError(error);\n    },\n    onSuccess: (data, variables) => {\n      options?.onSuccess?.(data, variables);\n      if (options?.context?.successMessage) {\n        toast.success('Success', { description: options.context.successMessage });\n      }\n    },\n  });\n}\n```\n\n**Requirements:**\n- Global error handler with toast notifications\n- Handle all HTTP status codes appropriately\n- Zod schema validation for response parsing\n- Automatic error display in toasts\n- Success message handling for mutations\n\n### 4. Parsing: Typed Responses, Optimistic Updates\n\n```typescript\n\u002F\u002F lib\u002Fapi\u002Ftypes.ts\nimport { z } from 'zod';\n\n\u002F\u002F Student type with Zod schema\nexport const StudentSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  email: z.string().email(),\n  role: z.enum(['student', 'teacher', 'admin']),\n  classId: z.string().nullable(),\n  createdAt: z.string(),\n  updatedAt: z.string(),\n});\n\nexport type Student = z.infer\u003Ctypeof StudentSchema>;\n\n\u002F\u002F Attendance type\nexport const AttendanceSchema = z.object({\n  id: z.string(),\n  studentId: z.string(),\n  date: z.string(),\n  status: z.enum(['present', 'absent', 'late']),\n  notes: z.string().optional(),\n});\n\nexport type Attendance = z.infer\u003Ctypeof AttendanceSchema>;\n\n\u002F\u002F Paginated response type\nexport function PaginatedResponseSchema\u003CT extends z.ZodTypeAny>(itemSchema: T) {\n  return z.object({\n    data: z.array(itemSchema),\n    meta: z.object({\n      total: z.number(),\n      page: z.number(),\n      pageSize: z.number(),\n      totalPages: z.number(),\n    }),\n  });\n}\n\n\u002F\u002F hooks\u002FuseStudents.ts\nimport { useApi } from '.\u002FuseApi';\nimport { StudentSchema, PaginatedResponseSchema } from '@\u002Flib\u002Fapi\u002Ftypes';\n\nexport function useStudents(page = 1, pageSize = 20) {\n  return useApi(\n    ['students', 'page', page],\n    `\u002Fstudents?page=${page}&pageSize=${pageSize}`,\n    {\n      select: (data) => {\n        const parsed = PaginatedResponseSchema(StudentSchema).parse(data);\n        return parsed;\n      },\n    }\n  );\n}\n\n\u002F\u002F hooks\u002FuseUpdateStudent.ts\nexport function useUpdateStudent() {\n  const queryClient = useQueryClient();\n\n  return useApiMutation(\n    (variables: { id: string; data: Partial\u003CStudent> }) =>\n      `\u002Fstudents\u002F${variables.id}`,\n    {\n      onSuccess: (_, variables) => {\n        \u002F\u002F Invalidate and refetch\n        queryClient.invalidateQueries({ queryKey: ['students'] });\n        queryClient.invalidateQueries({ queryKey: ['student', variables.id] });\n      },\n      context: { successMessage: 'Student updated successfully' },\n    }\n  );\n}\n\n\u002F\u002F hooks\u002FuseDeleteStudent.ts\nexport function useDeleteStudent() {\n  const queryClient = useQueryClient();\n\n  return useApiMutation(\n    (id: string) => `\u002Fstudents\u002F${id}`,\n    {\n      onSuccess: () => {\n        queryClient.invalidateQueries({ queryKey: ['students'] });\n      },\n      context: { successMessage: 'Student deleted successfully' },\n    }\n  );\n}\n```\n\n```typescript\n\u002F\u002F Infinite queries for pagination\nimport { useInfiniteQuery } from '@tanstack\u002Freact-query';\nimport { StudentSchema } from '@\u002Flib\u002Fapi\u002Ftypes';\n\nexport function useInfiniteStudents() {\n  return useInfiniteQuery({\n    queryKey: ['students', 'infinite'],\n    queryFn: async ({ pageParam = 1 }) => {\n      const response = await apiClient.get(`\u002Fstudents?page=${pageParam}&pageSize=20`);\n      const data = response.data.map((item: any) => StudentSchema.parse(item));\n      return {\n        data,\n        nextPage: data.length === 20 ? pageParam + 1 : null,\n      };\n    },\n    initialPageParam: 1,\n    getNextPageParam: (lastPage) => lastPage.nextPage,\n  });\n}\n\n\u002F\u002F Optimistic updates with rollback\nexport function useUpdateAttendance() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async ({ studentId, date, status }: { studentId: string; date: string; status: string }) => {\n      return apiClient.put(`\u002Fattendance\u002F${studentId}\u002F${date}`, { status });\n    },\n    onMutate: async ({ studentId, date, status }) => {\n      \u002F\u002F Cancel outgoing queries\n      await queryClient.cancelQueries({ queryKey: ['attendance', studentId] });\n\n      \u002F\u002F Snapshot previous value\n      const previousAttendance = queryClient.getQueryData(['attendance', studentId]);\n\n      \u002F\u002F Optimistically update\n      queryClient.setQueryData(['attendance', studentId], (old: any) => ({\n        ...old,\n        data: old.data.map((item: any) =>\n          item.date === date ? { ...item, status } : item\n        ),\n      }));\n\n      return { previousAttendance };\n    },\n    onError: (error, variables, context) => {\n      \u002F\u002F Rollback on error\n      if (context?.previousAttendance) {\n        queryClient.setQueryData(['attendance', variables.studentId], context.previousAttendance);\n      }\n    },\n    onSettled: (_, __, variables) => {\n      \u002F\u002F Refetch on success or error\n      queryClient.invalidateQueries({ queryKey: ['attendance', variables.studentId] });\n    },\n  });\n}\n\n\u002F\u002F Offline detection\nexport function useOnlineStatus() {\n  const [isOnline, setIsOnline] = useState(navigator.onLine);\n\n  useEffect(() => {\n    const handleOnline = () => setIsOnline(true);\n    const handleOffline = () => setIsOnline(false);\n\n    window.addEventListener('online', handleOnline);\n    window.addEventListener('offline', handleOffline);\n\n    return () => {\n      window.removeEventListener('online', handleOnline);\n      window.removeEventListener('offline', handleOffline);\n    };\n  }, []);\n\n  return isOnline;\n}\n\n\u002F\u002F AbortController for cancelable requests\nexport function useFetchWithAbort\u003CT>(url: string) {\n  const [data, setData] = useState\u003CT | null>(null);\n  const [error, setError] = useState\u003CError | null>(null);\n  const [loading, setLoading] = useState(false);\n  const abortControllerRef = useRef\u003CAbortController | null>(null);\n\n  useEffect(() => {\n    return () => {\n      abortControllerRef.current?.abort();\n    };\n  }, []);\n\n  const fetchData = useCallback(async () => {\n    if (abortControllerRef.current) {\n      abortControllerRef.current.abort();\n    }\n\n    abortControllerRef.current = new AbortController();\n    setLoading(true);\n    setError(null);\n\n    try {\n      const response = await apiClient.get\u003CT>(url, {\n        signal: abortControllerRef.current.signal,\n      });\n      setData(response.data);\n    } catch (err) {\n      if (err instanceof Error && err.name !== 'AbortError') {\n        setError(err);\n      }\n    } finally {\n      setLoading(false);\n    }\n  }, [url]);\n\n  return { data, error, loading, refetch: fetchData, abort: () => abortControllerRef.current?.abort() };\n}\n```\n\n**Requirements:**\n- Infinite queries for paginated lists\n- Optimistic updates for immediate feedback\n- Rollback on error\n- Offline detection and handling\n- AbortController for cancelable requests\n\n## Output Requirements\n\n### Code Files\n\n1. **API Client**:\n   - `lib\u002FapiClient.ts` - Axios instance with interceptors\n   - `lib\u002FqueryClient.ts` - TanStack Query configuration\n\n2. **Error Handling**:\n   - `lib\u002FerrorHandler.ts` - Global error handler\n   - `hooks\u002FuseApi.ts` - Type-safe API hooks\n\n3. **Type Definitions**:\n   - `lib\u002Fapi\u002Ftypes.ts` - Zod schemas and types\n\n4. **Feature Hooks**:\n   - `hooks\u002FuseStudents.ts` - Student-specific hooks\n   - `hooks\u002FuseAttendance.ts` - Attendance-specific hooks\n\n### Integration Requirements\n\n- **@auth-integration**: Use JWT tokens from auth store\n- **@react-component**: Functional components with hooks\n- **@tailwind-css**: Responsive UI with mobile support\n\n### Documentation\n\n- **PHR**: Create Prompt History Record for API decisions\n- **ADR**: Document caching strategy, retry policy\n- **Comments**: Document API endpoints and data flow\n\n## Workflow\n\n1. **Setup API Client**\n   - Configure TanStack Query\n   - Create Axios instance\n   - Setup JWT interceptors\n\n2. **Define Types**\n   - Create Zod schemas\n   - Export TypeScript types\n\n3. **Create Hooks**\n   - Build useApi and useApiMutation\n   - Add feature-specific hooks\n   - Implement error handling\n\n4. **Integrate with Auth**\n   - Attach JWT tokens automatically\n   - Handle 401 responses\n   - Refresh tokens on expiry\n\n5. **Implement Features**\n   - Query hooks for data fetching\n   - Mutation hooks with optimistic updates\n   - Infinite queries for pagination\n\n6. **Test and Optimize**\n   - Test error scenarios\n   - Verify offline behavior\n   - Optimize caching strategy\n\n## Quality Checklist\n\nBefore completing any API client implementation:\n\n- [ ] **Typesafe Requests\u002FResponses**: Zod schemas for all data\n- [ ] **Retry on Fail**: Exponential backoff for retries\n- [ ] **Offline Detection**: Handle network disconnections\n- [ ] **AbortController**: Support cancelable requests\n- [ ] **JWT Auto-Attach**: Headers with Authorization Bearer\n- [ ] **Error Handling**: Global error handler with toasts\n- [ ] **401 Logout**: Automatic redirect on token expiry\n- [ ] **Zod Validation**: Response schema validation\n- [ ] **Optimistic Updates**: Immediate UI feedback\n- [ ] **Query Invalidation**: Automatic cache updates\n\n## Common Patterns\n\n### Fetch Student Data\n\n```typescript\n\u002F\u002F hooks\u002FuseStudent.ts\nexport function useStudent(id: string) {\n  return useApi(\n    ['student', id],\n    `\u002Fstudents\u002F${id}`,\n    {\n      enabled: !!id, \u002F\u002F Only fetch if id exists\n    }\n  );\n}\n\n\u002F\u002F Usage\nfunction StudentProfile({ studentId }: { studentId: string }) {\n  const { data: student, isLoading, error } = useStudent(studentId);\n\n  if (isLoading) return \u003CLoadingSkeleton \u002F>;\n  if (error) return \u003CErrorMessage error={error} \u002F>;\n\n  return (\n    \u003Cdiv>\n      \u003Ch1>{student?.name}\u003C\u002Fh1>\n      \u003Cp>{student?.email}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### API Error Toast with Zod Parse\n\n```typescript\n\u002F\u002F hooks\u002FuseCreateStudent.ts\nexport function useCreateStudent() {\n  const queryClient = useQueryClient();\n\n  return useApiMutation(\n    async (data: { name: string; email: string }) => {\n      const response = await apiClient.post('\u002Fstudents', data);\n      \u002F\u002F Zod validation\n      const parsed = StudentSchema.parse(response.data);\n      return parsed;\n    },\n    {\n      onSuccess: () => {\n        queryClient.invalidateQueries({ queryKey: ['students'] });\n      },\n      context: { successMessage: 'Student created successfully' },\n    }\n  );\n}\n\n\u002F\u002F Usage\nfunction CreateStudentForm() {\n  const { mutate: createStudent, isPending } = useCreateStudent();\n\n  const handleSubmit = (data: FormData) => {\n    createStudent(data);\n  };\n\n  return \u003Cform onSubmit={handleSubmit}>{\u002F* form fields *\u002F}\u003C\u002Fform>;\n}\n```\n\n### Paginated List with Infinite Query\n\n```typescript\n\u002F\u002F hooks\u002FuseInfiniteStudents.ts\nexport function useInfiniteStudents() {\n  return useInfiniteQuery({\n    queryKey: ['students', 'infinite'],\n    queryFn: async ({ pageParam = 1 }) => {\n      const response = await apiClient.get(`\u002Fstudents?page=${pageParam}&pageSize=20`);\n      const parsed = z.array(StudentSchema).parse(response.data);\n      return {\n        data: parsed,\n        nextPage: parsed.length === 20 ? pageParam + 1 : null,\n      };\n    },\n    initialPageParam: 1,\n    getNextPageParam: (lastPage) => lastPage.nextPage,\n  });\n}\n\n\u002F\u002F Usage\nfunction StudentList() {\n  const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteStudents();\n\n  return (\n    \u003Cdiv>\n      {data?.pages.map((page, i) => (\n        \u003Cdiv key={i}>\n          {page.data.map((student) => (\n            \u003CStudentCard key={student.id} student={student} \u002F>\n          ))}\n        \u003C\u002Fdiv>\n      ))}\n      {hasNextPage && (\n        \u003Cbutton\n          onClick={() => fetchNextPage()}\n          disabled={isFetchingNextPage}\n        >\n          {isFetchingNextPage ? 'Loading...' : 'Load More'}\n        \u003C\u002Fbutton>\n      )}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Attendance Fetch with Offline Support\n\n```typescript\n\u002F\u002F hooks\u002FuseAttendance.ts\nexport function useAttendance(studentId: string, date: string) {\n  const isOnline = useOnlineStatus();\n\n  return useApi(\n    ['attendance', studentId, date],\n    `\u002Fattendance\u002F${studentId}\u002F${date}`,\n    {\n      enabled: !!studentId && !!date && isOnline,\n      staleTime: 5 * 60 * 1000,\n    }\n  );\n}\n\n\u002F\u002F Usage\nfunction AttendanceCard({ studentId, date }: { studentId: string; date: string }) {\n  const { data: attendance, isLoading, error } = useAttendance(studentId, date);\n  const isOnline = useOnlineStatus();\n\n  if (!isOnline) {\n    return \u003COfflineMessage \u002F>;\n  }\n\n  if (isLoading) return \u003CLoadingSkeleton \u002F>;\n  if (error) return \u003CErrorMessage error={error} \u002F>;\n\n  return (\n    \u003Cdiv>\n      \u003Cp>Status: {attendance?.status}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## Caching Strategy\n\n```typescript\n\u002F\u002F lib\u002FqueryClient.ts\nexport const queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      \u002F\u002F Fresh data is considered stale after 5 minutes\n      staleTime: 5 * 60 * 1000,\n      \u002F\u002F Garbage collect unused queries after 10 minutes\n      gcTime: 10 * 60 * 1000,\n      \u002F\u002F Retry failed requests 3 times\n      retry: 3,\n      \u002F\u002F Exponential backoff: 1s, 2s, 4s (max 30s)\n      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),\n      \u002F\u002F Refetch on window focus (optional)\n      refetchOnWindowFocus: false,\n      \u002F\u002F Refetch on reconnect\n      refetchOnReconnect: true,\n    },\n  },\n});\n```\n\n## Environment Variables\n\n```bash\n# .env.local\nNEXT_PUBLIC_API_URL=http:\u002F\u002Flocalhost:3001\u002Fapi\n# For production\nNEXT_PUBLIC_API_URL=https:\u002F\u002Fapi.yourapp.com\n```\n\n## References\n\n- TanStack Query: https:\u002F\u002Ftanstack.com\u002Fquery\u002Flatest\n- Axios: https:\u002F\u002Faxios-http.com\n- Zod: https:\u002F\u002Fzod.dev\n- React Query Examples: https:\u002F\u002Ftanstack.com\u002Fquery\u002Flatest\u002Fdocs\u002Freact\u002Fexamples\n",{"name":5,"description":7},"main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002Faiskillstore\u002Fmarketplace\u002Fawais68\u002Fapi-client",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":67,"prompt":77,"zipAvailable":78},[68,71,74],{"tool":69,"command":70},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill api-client",{"tool":72,"command":73},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill api-client",{"tool":75,"command":76},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill api-client","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fawais68\u002Fapi-client\u002FSKILL.md",true,[80,86],{"path":81,"resourceType":82,"resourceTypeDesc":83,"sizeBytes":84,"sourceHtmlUrl":85},"customize.toml","OTHER","其他",271,"https:\u002F\u002Fgithub.com\u002Fcarl0sfelipe\u002FBTC-Daytrade-Tycoon\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fbmad-sprint-planning\u002Fcustomize.toml",{"path":87,"resourceType":88,"resourceTypeDesc":89,"sizeBytes":90,"sourceHtmlUrl":91},"skill-report.json","REFERENCE","参考文档",10956,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fawais68\u002Fapi-client\u002Fskill-report.json",[93,103,113,122,128,135],{"skillNo":94,"name":95,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":96,"categories":97,"keywords":99,"publishedAt":52},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[98],{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[100,101,102],"model","model hierarchy","model hierarchy skill",{"skillNo":104,"name":105,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":106,"categories":107,"keywords":109,"publishedAt":52},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[108],{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[110,111,112],"MCP","zoom","zoom mcp",{"skillNo":114,"name":115,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":116,"categories":117,"keywords":118,"publishedAt":52},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[119,120,121,111],"sdk","zoom apps sdk","apps",{"skillNo":123,"name":124,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":125,"categories":126,"keywords":127,"publishedAt":52},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[124],{"skillNo":129,"name":130,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":131,"categories":132,"keywords":134,"publishedAt":52},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[133],{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[130],{"skillNo":136,"name":137,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":138,"categories":139,"keywords":140,"publishedAt":52},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[137]]