Skip to main content

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

  1. Create a singleton instance for your API client to ensure consistent configuration
  2. Use interceptors for cross-cutting concerns like logging, auth, and error handling
  3. Type your responses using generics for type safety
  4. Handle errors gracefully with appropriate user feedback
  5. Use request cancellation for cleanup in components that unmount
  6. Configure retries for transient failures