| name | api-client |
| description | Centralized TypeScript API client with typed namespaces, automatic token refresh with request deduplication, TanStack Query integration, and consistent error handling. |
| license | MIT |
| compatibility | TypeScript/JavaScript |
| metadata | {"category":"api","time":"5h","source":"drift-masterguide"} |
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
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;
}
interface RequestOptions {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
body?: Record<string, unknown>;
params?: Record<string, string | number | boolean | undefined>;
?: ;
}
{
: ;
: | = ;
: | = ;
: ;
isRefreshing = ;
: <> | = ;
() {
. = options..(, );
. = options. || ( {});
}
(: , : ): {
. = accessToken;
. = refreshToken;
}
(): {
. = ;
. = ;
}
auth = {
:
.<{ : ; : }>(, {
: ,
: data,
}),
:
.<>(, {
: ,
: { : . },
: ,
}),
:
.<>(, { : }),
};
users = {
:
.<>(, { : }),
:
.<>(, { : , : data }),
};
request<T>(: , : ): <T> {
url = .(endpoint, options.);
: <, > = {
: ,
};
(.) {
headers[] = ;
}
response = (url, {
: options.,
headers,
: options. ? .(options.) : ,
});
(response. === && !options.) {
refreshed = .();
(refreshed) {
.<T>(endpoint, { ...options, : });
}
.();
(, , );
}
(!response.) {
.(response);
}
(response. === ) T;
.<T>( response.());
}
(): <> {
(!.) ;
(.) {
.!;
}
. = ;
. = .();
{
.;
} {
. = ;
. = ;
}
}
(): <> {
{
tokens = ..();
.(tokens., tokens.);
;
} {
.();
;
}
}
(: , ?: <, >): {
url = ();
(params) {
.(params).( {
(value !== ) url..(key, (value));
});
}
url.();
}
transformResponse<T>(: ): T {
.(data) T;
}
(: ): {
(.(obj)) obj.( .(item));
(obj !== && obj === ) {
.(
.(obj).( [
key.(, letter.()),
.(value),
])
);
}
obj;
}
(: ): <> {
{
data = response.();
(
data. || ,
data. || ,
response.,
data.
);
} {
(, , response.);
}
}
}
apiClient = ({
: process.. || ,
: {
( !== ) .. = ;
},
});
TanStack Query Integration
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;
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,
});
}
() {
queryClient = ();
({
:
apiClient..(data),
: {
apiClient.(response.., response..);
queryClient.(queryKeys..(), response.);
},
});
}
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