Skip to main content
tanstack-client-api-layer API client setup with fetch wrapper, error handling, authentication headers, and React Query integration for TanStack Start/Router.
Ir para a instalação Skills Marketplace Descubra e explore skills de IA criadas pela comunidade.
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Copiar promptMostrar detalhes do prompt Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
npx skills add https://github.com/agusmdev/fullstack-ai-template --skill tanstack-client-api-layerO comando permanece em uma só linha. Role horizontalmente para revisá-lo antes de copiar.
Prefere uma cópia local? Baixe os arquivos disponíveis atualmente no SkillsMP.
Baixar Zip Baixando... Mais deste repositório Codebase health scanner and technical debt tracker. Use when the user asks about code quality, technical debt, dead code, large files, god classes, duplicate functions, code smells, naming issues, import cycles, or coupling problems. Also use when asked for a health score, what to fix next, or to create a cleanup plan. Supports 29 languages.
Configure Alembic for async SQLAlchemy migrations with PostgreSQL
Create FastAPI application factory with lifespan, middleware, pagination, and router configuration
Ocupações relacionadas SOC
Baseado na classificação ocupacional SOC
name tanstack-client-api-layer description API client setup with fetch wrapper, error handling, authentication headers, and React Query integration for TanStack Start/Router.
TanStack Client API Layer
Overview
This skill covers setting up a robust API client layer for TanStack Start or TanStack Router projects. It includes a type-safe fetch wrapper, centralized error handling, authentication token management, and React Query integration.
Important: This skill works for both:
TanStack Start (SSR full-stack)
TanStack Router (SPA client-only)
Prerequisites
Existing TanStack Start or TanStack Router project
React Query installed (see tanstack-react-query-setup skill)
TypeScript configured with path aliases
Architecture Overview
The API client layer consists of:
API Client - Central fetch wrapper with error handling
Error Types - Custom error classes for different error scenarios
Response Types - Shared TypeScript interfaces for API responses
Interceptors - Request/response middleware for auth headers and logging
React Query Integration - Helper functions for mutations and queries
Step 1: Create Error Types
Create src/lib/api/errors.ts:
export class ApiError extends Error {
constructor (
message : ,
: ,
?:
) {
(message)
. =
}
}
{
( ) {
(message)
. =
}
}
{
( ) {
(message, , data)
. =
}
}
{
( ) {
(message, , data)
. =
}
}
{
( ) {
(message, , data)
. =
}
}
{
( ) {
(message, , data)
. =
}
}
string
public
status
number
public
data
unknown
super
this
name
'ApiError'
export
class
NetworkError
extends
Error
constructor
message : string = 'Network request failed'
super
this
name
'NetworkError'
export
class
ValidationError
extends
ApiError
constructor
message : string ,
public errors : Record <string , string []>,
data ?: unknown
super
422
this
name
'ValidationError'
export
class
UnauthorizedError
extends
ApiError
constructor
message : string = 'Unauthorized' , data ?: unknown
super
401
this
name
'UnauthorizedError'
export
class
ForbiddenError
extends
ApiError
constructor
message : string = 'Forbidden' , data ?: unknown
super
403
this
name
'ForbiddenError'
export
class
NotFoundError
extends
ApiError
constructor
message : string = 'Resource not found' , data ?: unknown
super
404
this
name
'NotFoundError'
Step 2: Create Response Types Create src/lib/api/types.ts:
export interface ApiResponse <T = unknown > {
data : T
message ?: string
errors ?: Record <string , string []>
}
export interface PaginatedResponse <T> {
data : T[]
meta : {
current_page : number
per_page : number
total : number
last_page : number
}
}
export type ApiMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
export interface RequestConfig extends RequestInit {
params ?: Record <string , string | number | boolean >
timeout ?: number
}
Step 3: Create API Client Create src/lib/api/client.ts:
import {
ApiError ,
NetworkError ,
ValidationError ,
UnauthorizedError ,
ForbiddenError ,
NotFoundError ,
} from './errors'
import type { RequestConfig } from './types'
class ApiClient {
private baseURL : string
private defaultHeaders : HeadersInit
constructor (baseURL : string = '' ) {
this .baseURL = baseURL || import .meta .env .VITE_API_URL || ''
this .defaultHeaders = {
'Content-Type' : 'application/json' ,
}
}
setBaseURL (url : string ) {
this .baseURL = url
}
setDefaultHeaders (headers : HeadersInit ) {
this .defaultHeaders = { ...this .defaultHeaders , ...headers }
}
setAuthToken (token : string | null ) {
if (token) {
this .defaultHeaders = {
...this .defaultHeaders ,
Authorization : `Bearer ${token} ` ,
}
} else {
const { Authorization , ...rest } = this .defaultHeaders as Record <string , string >
this .defaultHeaders = rest
}
}
private buildURL (endpoint : string , params ?: Record <string , string | number | boolean >): string {
const url = new URL (endpoint, this .baseURL )
if (params) {
Object .entries (params).forEach (([key, value] ) => {
url.searchParams .append (key, String (value))
})
}
return url.toString ()
}
private async handleResponse<T>(response : Response ): Promise <T> {
const contentType = response.headers .get ('content-type' )
const isJson = contentType?.includes ('application/json' )
let data : unknown
try {
data = isJson ? await response.json () : await response.text ()
} catch (error) {
throw new ApiError ('Failed to parse response' , response.status )
}
if (!response.ok ) {
const message =
(isJson && typeof data === 'object' && data && 'message' in data)
? String (data.message )
: response.statusText
switch (response.status ) {
case 401 :
throw new UnauthorizedError (message, data)
case 403 :
throw new ForbiddenError (message, data)
case 404 :
throw new NotFoundError (message, data)
case 422 :
const errors =
(isJson && typeof data === 'object' && data && 'errors' in data)
? (data.errors as Record <string , string []>)
: {}
throw new ValidationError (message, errors, data)
default :
throw new ApiError (message, response.status , data)
}
}
return data as T
}
private async fetchWithTimeout (
url : string ,
config : RequestConfig
): Promise <Response > {
const { timeout = 30000 , ...fetchConfig } = config
const controller = new AbortController ()
const timeoutId = setTimeout (() => controller.abort (), timeout)
try {
const response = await fetch (url, {
...fetchConfig,
signal : controller.signal ,
})
clearTimeout (timeoutId)
return response
} catch (error) {
clearTimeout (timeoutId)
if (error instanceof Error ) {
if (error.name === 'AbortError' ) {
throw new NetworkError ('Request timeout' )
}
throw new NetworkError (error.message )
}
throw new NetworkError ('Unknown network error' )
}
}
async request<T = unknown >(
endpoint : string ,
config : RequestConfig = {}
): Promise <T> {
const { params, headers, ...restConfig } = config
const url = this .buildURL (endpoint, params)
const mergedHeaders = {
...this .defaultHeaders ,
...headers,
}
const response = await this .fetchWithTimeout (url, {
...restConfig,
headers : mergedHeaders,
})
return this .handleResponse <T>(response)
}
async get<T = unknown >(
endpoint : string ,
config ?: RequestConfig
): Promise <T> {
return this .request <T>(endpoint, { ...config, method : 'GET' })
}
async post<T = unknown >(
endpoint : string ,
body ?: unknown ,
config ?: RequestConfig
): Promise <T> {
return this .request <T>(endpoint, {
...config,
method : 'POST' ,
body : JSON .stringify (body),
})
}
async put<T = unknown >(
endpoint : string ,
body ?: unknown ,
config ?: RequestConfig
): Promise <T> {
return this .request <T>(endpoint, {
...config,
method : 'PUT' ,
body : JSON .stringify (body),
})
}
async patch<T = unknown >(
endpoint : string ,
body ?: unknown ,
config ?: RequestConfig
): Promise <T> {
return this .request <T>(endpoint, {
...config,
method : 'PATCH' ,
body : JSON .stringify (body),
})
}
async delete <T = unknown >(
endpoint : string ,
config ?: RequestConfig
): Promise <T> {
return this .request <T>(endpoint, { ...config, method : 'DELETE' })
}
}
export const apiClient = new ApiClient ()
export { ApiClient }
Step 4: Create Environment Configuration Add to .env or .env.local:
VITE_API_URL=http://localhost:8000
VITE_API_URL=https://api.yourapp.com
Step 5: Create API Service Example Create src/lib/api/services/users.ts:
import { apiClient } from '../client'
import type { ApiResponse } from '../types'
export interface User {
id : number
email : string
name : string
created_at : string
updated_at : string
}
export interface CreateUserDto {
email : string
name : string
password : string
}
export interface UpdateUserDto {
email ?: string
name ?: string
password ?: string
}
export const usersApi = {
getAll : () =>
apiClient.get <ApiResponse <User []>>('/api/users' ),
getById : (id : number ) =>
apiClient.get <ApiResponse <User >>(`/api/users/${id} ` ),
create : (data : CreateUserDto ) =>
apiClient.post <ApiResponse <User >>('/api/users' , data),
update : (id : number , data : UpdateUserDto ) =>
apiClient.patch <ApiResponse <User >>(`/api/users/${id} ` , data),
delete : (id : number ) =>
apiClient.delete <ApiResponse <void >>(`/api/users/${id} ` ),
search : (query : string , page : number = 1 ) =>
apiClient.get <ApiResponse <User []>>('/api/users/search' , {
params : { q : query, page },
}),
}
Step 6: React Query Integration Create src/lib/api/hooks/useUsers.ts:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { usersApi } from '../services/users'
import type { CreateUserDto , UpdateUserDto } from '../services/users'
export const userKeys = {
all : ['users' ] as const ,
lists : () => [...userKeys.all , 'list' ] as const ,
list : (filters : string ) => [...userKeys.lists (), { filters }] as const ,
details : () => [...userKeys.all , 'detail' ] as const ,
detail : (id : number ) => [...userKeys.details (), id] as const ,
}
export function useUsers ( ) {
return useQuery ({
queryKey : userKeys.lists (),
queryFn : async () => {
const response = await usersApi.getAll ()
return response.data
},
})
}
export function useUser (id : number ) {
return useQuery ({
queryKey : userKeys.detail (id),
queryFn : async () => {
const response = await usersApi.getById (id)
return response.data
},
enabled : !!id,
})
}
export function useCreateUser ( ) {
const queryClient = useQueryClient ()
return useMutation ({
mutationFn : (data : CreateUserDto ) => usersApi.create (data),
onSuccess : () => {
queryClient.invalidateQueries ({ queryKey : userKeys.lists () })
},
})
}
export function useUpdateUser ( ) {
const queryClient = useQueryClient ()
return useMutation ({
mutationFn : ({ id, data }: { id: number ; data: UpdateUserDto } ) =>
usersApi.update (id, data),
onSuccess : (response, variables ) => {
queryClient.invalidateQueries ({ queryKey : userKeys.detail (variables.id ) })
queryClient.invalidateQueries ({ queryKey : userKeys.lists () })
},
})
}
export function useDeleteUser ( ) {
const queryClient = useQueryClient ()
return useMutation ({
mutationFn : (id : number ) => usersApi.delete (id),
onSuccess : () => {
queryClient.invalidateQueries ({ queryKey : userKeys.lists () })
},
})
}
export function useSearchUsers (query : string , page : number = 1 ) {
return useQuery ({
queryKey : [...userKeys.lists (), 'search' , query, page],
queryFn : async () => {
const response = await usersApi.search (query, page)
return response.data
},
enabled : query.length > 0 ,
})
}
Step 7: Error Handling in Components Create src/components/error-boundary.tsx:
import { Component , type ReactNode } from 'react'
import { ApiError , NetworkError , UnauthorizedError } from '~/lib/api/errors'
interface Props {
children : ReactNode
fallback ?: (error : Error ) => ReactNode
}
interface State {
hasError : boolean
error : Error | null
}
export class ErrorBoundary extends Component <Props , State > {
constructor (props : Props ) {
super (props)
this .state = { hasError : false , error : null }
}
static getDerivedStateFromError (error : Error ): State {
return { hasError : true , error }
}
componentDidCatch (error : Error ) {
console .error ('ErrorBoundary caught:' , error)
}
render ( ) {
if (this .state .hasError && this .state .error ) {
if (this .props .fallback ) {
return this .props .fallback (this .state .error )
}
return <DefaultErrorFallback error ={this.state.error} />
}
return this .props .children
}
}
function DefaultErrorFallback ({ error }: { error: Error } ) {
if (error instanceof UnauthorizedError ) {
return (
<div className ="flex min-h-screen items-center justify-center" >
<div className ="text-center" >
<h1 className ="text-2xl font-bold" > Unauthorized</h1 >
<p className ="text-muted-foreground" > Please log in to continue</p >
</div >
</div >
)
}
if (error instanceof NetworkError ) {
return (
<div className ="flex min-h-screen items-center justify-center" >
<div className ="text-center" >
<h1 className ="text-2xl font-bold" > Network Error</h1 >
<p className ="text-muted-foreground" >
Unable to connect to the server. Please check your connection.
</p >
</div >
</div >
)
}
if (error instanceof ApiError ) {
return (
<div className ="flex min-h-screen items-center justify-center" >
<div className ="text-center" >
<h1 className ="text-2xl font-bold" > Error {error.status}</h1 >
<p className ="text-muted-foreground" > {error.message}</p >
</div >
</div >
)
}
return (
<div className ="flex min-h-screen items-center justify-center" >
<div className ="text-center" >
<h1 className ="text-2xl font-bold" > Something went wrong</h1 >
<p className ="text-muted-foreground" > {error.message}</p >
</div >
</div >
)
}
Step 8: Usage in Components Example component using the API client:
import { useUsers, useCreateUser, useDeleteUser } from '~/lib/api/hooks/useUsers'
import { Button } from '~/components/ui/button'
import { useToast } from '~/hooks/use-toast'
export default function UsersPage ( ) {
const { data : users, isLoading, error } = useUsers ()
const createUser = useCreateUser ()
const deleteUser = useDeleteUser ()
const { toast } = useToast ()
const handleCreate = async ( ) => {
try {
await createUser.mutateAsync ({
email : 'new@example.com' ,
name : 'New User' ,
password : 'password123' ,
})
toast ({ title : 'User created successfully' })
} catch (error) {
if (error instanceof ValidationError ) {
toast ({
title : 'Validation failed' ,
description : Object .values (error.errors ).flat ().join (', ' ),
variant : 'destructive' ,
})
} else {
toast ({
title : 'Failed to create user' ,
variant : 'destructive' ,
})
}
}
}
const handleDelete = async (id : number ) => {
try {
await deleteUser.mutateAsync (id)
toast ({ title : 'User deleted successfully' })
} catch (error) {
toast ({
title : 'Failed to delete user' ,
variant : 'destructive' ,
})
}
}
if (isLoading) return <div > Loading...</div >
if (error) return <div > Error: {error.message}</div >
return (
<div >
<div className ="mb-4" >
<Button onClick ={handleCreate} disabled ={createUser.isPending} >
{createUser.isPending ? 'Creating...' : 'Create User'}
</Button >
</div >
<div className ="space-y-2" >
{users?.map((user) => (
<div key ={user.id} className ="flex items-center justify-between" >
<span > {user.name} ({user.email})</span >
<Button
variant ="destructive"
size ="sm"
onClick ={() => handleDelete(user.id)}
disabled={deleteUser.isPending}
>
Delete
</Button >
</div >
))}
</div >
</div >
)
}
Step 9: Authentication Integration Create src/lib/api/auth.ts:
import { apiClient } from './client'
export function initializeAuth ( ) {
const token = localStorage .getItem ('auth_token' )
if (token) {
apiClient.setAuthToken (token)
}
}
export function setAuthToken (token : string ) {
localStorage .setItem ('auth_token' , token)
apiClient.setAuthToken (token)
}
export function clearAuthToken ( ) {
localStorage .removeItem ('auth_token' )
apiClient.setAuthToken (null )
}
Call initializeAuth() in your root component or router setup:
import { useEffect } from 'react'
import { initializeAuth } from '~/lib/api/auth'
export function App ( ) {
useEffect (() => {
initializeAuth ()
}, [])
return <div > {/* Your app */}</div >
}
Project Structure After Setup your-project/
├── src/
│ ├── lib/
│ │ └── api/
│ │ ├── client.ts # API client with fetch wrapper
│ │ ├── errors.ts # Custom error types
│ │ ├── types.ts # Shared API types
│ │ ├── auth.ts # Auth token management
│ │ ├── services/ # API service modules
│ │ │ ├── users.ts
│ │ │ └── ...
│ │ └── hooks/ # React Query hooks
│ │ ├── useUsers.ts
│ │ └── ...
│ ├── components/
│ │ └── error-boundary.tsx # Error handling component
│ └── routes/ # TanStack routes
├── .env # Environment variables
└── vite.config.ts
Advanced Features
Request Interceptors Add logging or modify requests globally:
async request<T = unknown >(
endpoint : string ,
config : RequestConfig = {}
): Promise <T> {
if (import .meta .env .DEV ) {
console .log (`[API] ${config.method || 'GET' } ${endpoint} ` , config)
}
const { params, headers, ...restConfig } = config
}
Response Caching private cache = new Map <string , { data : unknown ; timestamp : number }>()
private cacheTimeout = 5 * 60 * 1000
private getCacheKey (url : string , config : RequestConfig ): string {
return `${url} :${JSON .stringify(config)} `
}
private getFromCache<T>(key : string ): T | null {
const cached = this .cache .get (key)
if (cached && Date .now () - cached.timestamp < this .cacheTimeout ) {
return cached.data as T
}
return null
}
private setCache (key : string , data : unknown ): void {
this .cache .set (key, { data, timestamp : Date .now () })
}
Retry Logic Add automatic retry for failed requests:
async requestWithRetry<T>(
endpoint : string ,
config : RequestConfig = {},
retries : number = 3
): Promise <T> {
try {
return await this .request <T>(endpoint, config)
} catch (error) {
if (retries > 0 && error instanceof NetworkError ) {
await new Promise (resolve => setTimeout (resolve, 1000 ))
return this .requestWithRetry <T>(endpoint, config, retries - 1 )
}
throw error
}
}
Testing Create src/lib/api/__tests__/client.test.ts:
import { describe, it, expect, vi } from 'vitest'
import { ApiClient } from '../client'
import { ApiError , NetworkError } from '../errors'
describe ('ApiClient' , () => {
it ('should make GET request' , async () => {
global .fetch = vi.fn ().mockResolvedValue ({
ok : true ,
json : async () => ({ data : 'test' }),
headers : new Headers ({ 'content-type' : 'application/json' }),
})
const client = new ApiClient ('https://api.example.com' )
const result = await client.get ('/test' )
expect (result).toEqual ({ data : 'test' })
})
it ('should throw ApiError on 500' , async () => {
global .fetch = vi.fn ().mockResolvedValue ({
ok : false ,
status : 500 ,
statusText : 'Internal Server Error' ,
json : async () => ({ message : 'Server error' }),
headers : new Headers ({ 'content-type' : 'application/json' }),
})
const client = new ApiClient ()
await expect (client.get ('/test' )).rejects .toThrow (ApiError )
})
it ('should handle network errors' , async () => {
global .fetch = vi.fn ().mockRejectedValue (new Error ('Network failed' ))
const client = new ApiClient ()
await expect (client.get ('/test' )).rejects .toThrow (NetworkError )
})
})
Common Issues
Issue: CORS errors in development Solution: Configure Vite proxy in vite.config.ts:
export default defineConfig ({
server : {
proxy : {
'/api' : {
target : 'http://localhost:8000' ,
changeOrigin : true ,
},
},
},
})
Issue: Stale data after mutations Solution: Use proper query invalidation in mutation onSuccess:
onSuccess : () => {
queryClient.invalidateQueries ({ queryKey : userKeys.lists () })
}
Issue: Type errors with generic responses Solution: Explicitly type the response:
const response = await apiClient.get <ApiResponse <User []>>('/api/users' )
Notes
API client is a singleton by default for shared state
Authentication tokens are automatically added to all requests
Errors are typed for better error handling
Works seamlessly with React Query for caching and refetching
Timeout defaults to 30 seconds per request
All requests use JSON by default
Next Steps
Create service modules for each API resource
Add React Query hooks for data fetching
Implement authentication flows (see tanstack-client-auth skill)
Add optimistic updates for better UX
Set up error boundaries for graceful error handling
Resources