ApiClient
The ApiClient class provides a type-safe HTTP client for making API requests with built-in support for authentication, interceptors, and error handling.
Installation
npm install @54vie/api
# or
yarn add @54vie/api
# or
pnpm add @54vie/api
TypeScript Interfaces
interface ApiClientConfig {
baseURL: string;
timeout?: number;
headers?: Record<string, string>;
withCredentials?: boolean;
retryConfig?: RetryConfig;
authConfig?: AuthConfig;
}
interface RetryConfig {
maxRetries?: number;
retryDelay?: number;
retryCondition?: (error: ApiError) => boolean;
}
interface AuthConfig {
type: 'bearer' | 'basic' | 'api-key' | 'custom';
token?: string | (() => string | Promise<string>);
headerName?: string;
refreshToken?: () => Promise<string>;
onTokenRefresh?: (newToken: string) => void;
}
interface RequestConfig<T = unknown> {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
data?: T;
params?: Record<string, string | number | boolean>;
headers?: Record<string, string>;
timeout?: number;
signal?: AbortSignal;
responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
}
interface ApiResponse<T> {
data: T;
status: number;
statusText: string;
headers: Record<string, string>;
}
interface ApiError {
message: string;
status?: number;
code?: string;
data?: unknown;
originalError?: Error;
}
interface Interceptor<T> {
onFulfilled?: (value: T) => T | Promise<T>;
onRejected?: (error: ApiError) => ApiError | Promise<ApiError>;
}
type RequestInterceptor = Interceptor<RequestConfig>;
type ResponseInterceptor = Interceptor<ApiResponse<unknown>>;
Creating an ApiClient Instance
import { ApiClient } from '@54vie/api';
const client = new ApiClient({
baseURL: 'https://api.54vie.com/v1',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
});
HTTP Methods
GET Request
// Simple GET request
const response = await client.get<User>('/users/123');
console.log(response.data);
// GET with query parameters
const users = await client.get<User[]>('/users', {
params: {
page: 1,
limit: 10,
status: 'active',
},
});
// GET with custom headers
const profile = await client.get<Profile>('/profile', {
headers: {
'X-Custom-Header': 'value',
},
});
POST Request
// Create a new resource
const newUser = await client.post<User>('/users', {
data: {
name: 'John Doe',
email: 'john@example.com',
},
});
// POST with form data
const formData = new FormData();
formData.append('file', file);
formData.append('name', 'document.pdf');
const upload = await client.post<UploadResponse>('/files/upload', {
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
});
PUT Request
// Full resource update
const updatedUser = await client.put<User>('/users/123', {
data: {
name: 'John Updated',
email: 'john.updated@example.com',
role: 'admin',
},
});
PATCH Request
// Partial resource update
const patchedUser = await client.patch<User>('/users/123', {
data: {
name: 'John Patched',
},
});
DELETE Request
// Delete a resource
await client.delete('/users/123');
// Delete with confirmation body
await client.delete('/users/123', {
data: {
confirm: true,
reason: 'User requested account deletion',
},
});
Configuration Options
Full Configuration Example
const client = new ApiClient({
// Base URL for all requests
baseURL: 'https://api.54vie.com/v1',
// Request timeout in milliseconds
timeout: 30000,
// Default headers for all requests
headers: {
'Content-Type': 'application/json',
'X-App-Version': '1.0.0',
},
// Include credentials in cross-origin requests
withCredentials: true,
// Retry configuration
retryConfig: {
maxRetries: 3,
retryDelay: 1000,
retryCondition: (error) => {
// Retry on network errors or 5xx status codes
return !error.status || error.status >= 500;
},
},
// Authentication configuration
authConfig: {
type: 'bearer',
token: () => localStorage.getItem('accessToken') || '',
refreshToken: async () => {
const response = await fetch('/auth/refresh', {
method: 'POST',
credentials: 'include',
});
const data = await response.json();
return data.accessToken;
},
onTokenRefresh: (newToken) => {
localStorage.setItem('accessToken', newToken);
},
},
});
Authentication Headers
Bearer Token Authentication
const client = new ApiClient({
baseURL: 'https://api.54vie.com/v1',
authConfig: {
type: 'bearer',
token: 'your-jwt-token',
},
});
// Results in header: Authorization: Bearer your-jwt-token
Dynamic Token
const client = new ApiClient({
baseURL: 'https://api.54vie.com/v1',
authConfig: {
type: 'bearer',
token: () => {
// Dynamically get token from storage or state
return localStorage.getItem('accessToken') || '';
},
},
});
API Key Authentication
const client = new ApiClient({
baseURL: 'https://api.54vie.com/v1',
authConfig: {
type: 'api-key',
token: 'your-api-key',
headerName: 'X-API-Key', // Custom header name
},
});
// Results in header: X-API-Key: your-api-key
Basic Authentication
const client = new ApiClient({
baseURL: 'https://api.54vie.com/v1',
authConfig: {
type: 'basic',
token: btoa('username:password'),
},
});
// Results in header: Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
Custom Authentication
const client = new ApiClient({
baseURL: 'https://api.54vie.com/v1',
authConfig: {
type: 'custom',
token: async () => {
// Custom logic to get auth token
const session = await getSession();
return `Custom ${session.token}`;
},
headerName: 'Authorization',
},
});
Interceptors
Request Interceptors
// Add request interceptor
const requestInterceptorId = client.interceptors.request.use({
onFulfilled: (config) => {
// Add timestamp to all requests
config.headers = {
...config.headers,
'X-Request-Time': Date.now().toString(),
};
console.log(`[Request] ${config.method} ${config.url}`);
return config;
},
onRejected: (error) => {
console.error('[Request Error]', error);
return Promise.reject(error);
},
});
// Remove request interceptor
client.interceptors.request.eject(requestInterceptorId);
Response Interceptors
// Add response interceptor
const responseInterceptorId = client.interceptors.response.use({
onFulfilled: (response) => {
// Log successful responses
console.log(`[Response] ${response.status} ${response.statusText}`);
// Transform response data if needed
return {
...response,
data: {
...response.data,
receivedAt: new Date().toISOString(),
},
};
},
onRejected: async (error) => {
// Handle 401 errors globally
if (error.status === 401) {
// Attempt token refresh
try {
await client.refreshAuth();
// Retry the original request
return client.request(error.config);
} catch (refreshError) {
// Redirect to login
window.location.href = '/login';
}
}
return Promise.reject(error);
},
});
// Remove response interceptor
client.interceptors.response.eject(responseInterceptorId);
Logging Interceptor Example
// Create a comprehensive logging interceptor
client.interceptors.request.use({
onFulfilled: (config) => {
const requestId = crypto.randomUUID();
config.headers = {
...config.headers,
'X-Request-ID': requestId,
};
console.group(`API Request [${requestId}]`);
console.log('Method:', config.method);
console.log('URL:', config.url);
console.log('Params:', config.params);
console.log('Data:', config.data);
console.groupEnd();
return config;
},
});
client.interceptors.response.use({
onFulfilled: (response) => {
const requestId = response.headers['x-request-id'];
console.group(`API Response [${requestId}]`);
console.log('Status:', response.status);
console.log('Data:', response.data);
console.groupEnd();
return response;
},
onRejected: (error) => {
console.group('API Error');
console.error('Status:', error.status);
console.error('Message:', error.message);
console.error('Data:', error.data);
console.groupEnd();
return Promise.reject(error);
},
});
Error Handling
import { ApiClient, ApiError, isApiError } from '@54vie/api';
try {
const response = await client.get<User>('/users/123');
console.log(response.data);
} catch (error) {
if (isApiError(error)) {
switch (error.status) {
case 400:
console.error('Bad Request:', error.data);
break;
case 401:
console.error('Unauthorized');
// Redirect to login
break;
case 403:
console.error('Forbidden');
break;
case 404:
console.error('Not Found');
break;
case 429:
console.error('Rate Limited');
break;
case 500:
console.error('Server Error');
break;
default:
console.error('Unknown Error:', error.message);
}
} else {
console.error('Network Error:', error);
}
}
Request Cancellation
// Using AbortController
const controller = new AbortController();
// Make request with signal
const responsePromise = client.get<User[]>('/users', {
signal: controller.signal,
});
// Cancel the request
controller.abort();
// Handle cancellation
try {
const response = await responsePromise;
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
}
}
Creating Multiple Instances
// Main API client
const mainClient = new ApiClient({
baseURL: 'https://api.54vie.com/v1',
authConfig: {
type: 'bearer',
token: () => getAccessToken(),
},
});
// Admin API client with different configuration
const adminClient = new ApiClient({
baseURL: 'https://admin.54vie.com/api',
authConfig: {
type: 'api-key',
token: process.env.ADMIN_API_KEY,
headerName: 'X-Admin-Key',
},
});
// Third-party API client
const externalClient = new ApiClient({
baseURL: 'https://external-api.com',
timeout: 60000,
retryConfig: {
maxRetries: 5,
retryDelay: 2000,
},
});
Best Practices
- Create a singleton instance for your API client to ensure consistent configuration
- Use interceptors for cross-cutting concerns like logging, auth, and error handling
- Type your responses using generics for type safety
- Handle errors gracefully with appropriate user feedback
- Use request cancellation for cleanup in components that unmount
- Configure retries for transient failures
Related
- React Hooks - useQuery, useMutation hooks for React
- WebSocket Client - Real-time communication
- Caching - Response caching and offline support