A lightweight, modern TypeScript HTTP client library built on the native fetch API. This library provides a powerful alternative to axios with advanced features like interceptors, automatic retries, comprehensive error handling, and full TypeScript support.
- Features
- Installation
- Quick Start
- API Reference
- Advanced Usage
- TypeScript Support
- Examples
- API Documentation
- Contributing
- License
- Lightweight: Built on native fetch API with zero dependencies
- TypeScript Support: Full type safety with comprehensive type definitions
- Interceptors: Request and response middleware for authentication, logging, and transformation
- Automatic Retries: Configurable retry logic with exponential backoff
- Error Handling: Comprehensive error management with detailed error codes
- Response Parsing: Automatic response parsing based on content type
- Timeout Support: Request timeout handling with customizable timeouts
- Base URL Support: Configure base URLs for consistent API endpoints
- Header Management: Global and per-request header configuration
- Modern ES6+: Uses modern JavaScript features with excellent browser support
The library provides the following exports organized by category:
npm install @x-common/fetch-clientyarn add @x-common/fetch-clientpnpm add @x-common/fetch-clientThis package supports ESM, CommonJS, and Browser environments:
import { Client, HTTP_STATUS } from '@x-common/fetch-client';
import type { RequestConfig } from '@x-common/fetch-client';const { Client, HTTP_STATUS } = require('@x-common/fetch-client');<!-- For modern browsers (ESM) -->
<script type="module">
import { Client } from 'https://unpkg.com/@x-common/fetch-client/dist/browser/index.esm.min.js';
const api = new Client({ baseURL: 'https://api.example.com' });
</script>
<!-- For older browsers (UMD) -->
<script src="https://unpkg.com/@x-common/fetch-client/dist/browser/index.umd.min.js"></script>
<script>
const { Client } = FetchClient;
const api = new Client({ baseURL: 'https://api.example.com' });
</script>React/Next.js:
import { Client } from '@x-common/fetch-client';
const api = new Client({ baseURL: process.env.NEXT_PUBLIC_API_URL });Vue/Nuxt:
import { Client } from '@x-common/fetch-client';
export const $api = new Client({ baseURL: useRuntimeConfig().public.apiBase });Angular:
import { Injectable } from '@angular/core';
import { Client } from '@x-common/fetch-client';
@Injectable({ providedIn: 'root' })
export class ApiService extends Client {
constructor() {
super({ baseURL: environment.apiUrl });
}
}The package automatically resolves to the correct module format based on your environment.
import { Client } from '@x-common/fetch-client';
// Create a client instance
const api = new Client({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// Make requests
const users = await api.get('/users');
const newUser = await api.post('/users', {
name: 'John Doe',
email: 'john@example.com'
});import { Client } from '@x-common/fetch-client';
const api = new Client({
baseURL: 'https://api.example.com',
headers: {
'Authorization': 'Bearer your-access-token',
'Content-Type': 'application/json'
}
});
// All requests will include the Authorization header
const profile = await api.get('/profile');The Client constructor accepts a configuration object with the following options:
interface ClientConfig {
baseURL?: string; // Base URL for all requests
timeout?: number; // Request timeout in milliseconds
headers?: Record<string, string>; // Default headers for all requests
retry?: RetryConfig; // Retry configuration
responseType?: ResponseType; // Default response parsing type
}// Minimal configuration
const api = new Client();
// Basic configuration
const api = new Client({
baseURL: 'https://api.example.com'
});
// Full configuration
const api = new Client({
baseURL: 'https://api.example.com',
timeout: 15000,
headers: {
'User-Agent': 'MyApp/1.0.0',
'Accept': 'application/json',
'Authorization': 'Bearer token'
},
retry: {
maxRetries: 3,
delay: 1000
},
responseType: 'json'
});The client supports all standard HTTP methods:
// Simple GET request
const users = await api.get('/users');
// GET with query parameters
const users = await api.get('/users', {
params: { page: 1, limit: 10 }
});
// GET with custom headers
const users = await api.get('/users', {
headers: { 'Cache-Control': 'no-cache' }
});// POST with JSON data
const newUser = await api.post('/users', {
name: 'John Doe',
email: 'john@example.com'
});
// POST with form data
const formData = new FormData();
formData.append('file', file);
const upload = await api.post('/upload', formData);
// POST with custom options
const result = await api.post('/users', userData, {
timeout: 30000,
headers: { 'X-Custom-Header': 'value' }
});// Update entire resource
const updatedUser = await api.put('/users/123', {
name: 'Jane Doe',
email: 'jane@example.com',
status: 'active'
});// Partial update
const updatedUser = await api.patch('/users/123', {
status: 'inactive'
});// Delete resource
await api.delete('/users/123');
// Delete with confirmation
await api.delete('/users/123', {
headers: { 'X-Confirm': 'true' }
});// Get headers only
const headers = await api.head('/users/123');Each request method accepts an optional configuration object:
interface RequestOptions {
headers?: Record<string, string>; // Request-specific headers
timeout?: number; // Request-specific timeout
retry?: RetryConfig; // Request-specific retry config
responseType?: ResponseType; // How to parse the response
params?: Record<string, string>; // Query parameters
}The client automatically parses responses based on the Content-Type header:
// JSON response (Content-Type: application/json)
const data = await api.get('/users'); // Returns parsed object
// Text response (Content-Type: text/plain)
const text = await api.get('/status'); // Returns string
// Binary response (Content-Type: application/octet-stream)
const blob = await api.get('/download'); // Returns BlobYou can specify the response type explicitly:
// Force JSON parsing
const data = await api.get('/data', { responseType: 'json' });
// Force text parsing
const text = await api.get('/data', { responseType: 'text' });
// Get as Blob
const blob = await api.get('/file', { responseType: 'blob' });
// Get as ArrayBuffer
const buffer = await api.get('/binary', { responseType: 'arrayBuffer' });For full control over response handling, use the request method:
const response = await api.request({
url: '/users',
method: 'GET'
});
// Access response properties
console.log(response.status); // HTTP status code
console.log(response.statusText); // HTTP status text
console.log(response.headers); // Response headers
// Get raw response data
const rawData = response.getRawData();
const parsedData = await response.getData();The client provides comprehensive error handling with detailed error information:
import { ApiError, ERROR_CODES } from '@x-common/fetch-client';
try {
const data = await api.get('/protected');
} catch (error) {
if (error instanceof ApiError) {
console.log('Error Code:', error.code);
console.log('HTTP Status:', error.status);
console.log('Message:', error.message);
console.log('Request URL:', error.request.url);
// Handle specific error types
switch (error.code) {
case ERROR_CODES.NETWORK_ERROR:
console.log('Network connection failed');
break;
case ERROR_CODES.TIMEOUT_ERROR:
console.log('Request timed out');
break;
case ERROR_CODES.HTTP_ERROR:
console.log('Server returned error status');
break;
case ERROR_CODES.PARSE_ERROR:
console.log('Failed to parse response');
break;
}
}
}NETWORK_ERROR: Network connection issuesTIMEOUT_ERROR: Request exceeded timeout limitHTTP_ERROR: Server returned error status (4xx, 5xx)PARSE_ERROR: Failed to parse response dataVALIDATION_ERROR: Request validation failedABORT_ERROR: Request was cancelledUNKNOWN_ERROR: Unexpected error occurred
Interceptors allow you to transform requests and responses globally:
// Add authentication to all requests
api.interceptors.request.use((request) => {
const token = localStorage.getItem('authToken');
if (token) {
request.headers.set('Authorization', `Bearer ${token}`);
}
return request;
});
// Log all outgoing requests
api.interceptors.request.use((request) => {
console.log(`Sending ${request.method} request to ${request.url}`);
return request;
});
// Modify request data
api.interceptors.request.use((request) => {
if (request.method === 'POST' && request.body) {
// Add timestamp to all POST requests
const data = JSON.parse(request.body as string);
data.timestamp = new Date().toISOString();
request.body = JSON.stringify(data);
}
return request;
});// Handle authentication errors globally
api.interceptors.response.use((response) => {
if (response.status === 401) {
// Redirect to login or refresh token
window.location.href = '/login';
}
return response;
});
// Log all responses
api.interceptors.response.use((response) => {
console.log(`Received ${response.status} response from ${response.request.url}`);
return response;
});
// Transform response data
api.interceptors.response.use((response) => {
// Add metadata to all responses
const originalGetData = response.getData.bind(response);
response.getData = async () => {
const data = await originalGetData();
return {
data,
requestTime: response.request.timestamp,
responseTime: Date.now()
};
};
return response;
});Configure automatic retries for failed requests:
const api = new Client({
retry: {
maxRetries: 3, // Maximum number of retry attempts
delay: 1000, // Initial delay between retries (ms)
shouldRetry: [408, 429, 500, 502, 503, 504] // Status codes to retry
}
});const api = new Client({
retry: {
maxRetries: 5,
delay: 2000,
// Custom retry logic
shouldRetry: (error) => {
// Retry on network errors
if (error.code === ERROR_CODES.NETWORK_ERROR) {
return true;
}
// Retry on specific HTTP status codes
if (error.status && [408, 429, 500, 502, 503, 504].includes(error.status)) {
return true;
}
return false;
}
}
});// Override retry config for specific requests
const data = await api.get('/critical-data', {
retry: {
maxRetries: 10,
delay: 5000
}
});Create different client instances for different APIs:
// Main API client
const mainApi = new Client({
baseURL: 'https://api.myapp.com',
headers: { 'Authorization': 'Bearer main-token' }
});
// Analytics API client
const analyticsApi = new Client({
baseURL: 'https://analytics.myapp.com',
headers: { 'X-API-Key': 'analytics-key' },
timeout: 5000
});
// File upload client
const uploadApi = new Client({
baseURL: 'https://upload.myapp.com',
timeout: 60000, // Longer timeout for uploads
retry: { maxRetries: 1 } // Fewer retries for uploads
});Update client configuration at runtime:
const api = new Client();
// Set headers dynamically
api.setHeader('Authorization', `Bearer ${newToken}`);
api.setHeader('X-Client-Version', '2.0.0');
// Remove headers
api.removeHeader('Authorization');
// Get current configuration
const config = api.getConfig();
console.log(config.baseURL, config.headers);Handle file uploads with progress tracking:
// Single file upload
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'Profile picture');
const result = await api.post('/upload', formData, {
timeout: 60000, // 60 second timeout for uploads
headers: {
// Don't set Content-Type - let browser set it with boundary
}
});
// Multiple file upload
const files = Array.from(fileInput.files);
const formData = new FormData();
files.forEach((file, index) => {
formData.append(`file${index}`, file);
});
const result = await api.post('/upload-multiple', formData);Manage request timeouts to prevent long-running requests:
// Global timeout configuration
const api = new Client({
baseURL: 'https://api.example.com',
timeout: 10000 // 10 seconds timeout for all requests
});
// Per-request timeout override
try {
const result = await api.get('/long-running-request', {
timeout: 30000 // 30 seconds timeout for this specific request
});
} catch (error) {
if (error.code === ERROR_CODES.TIMEOUT_ERROR) {
console.log('Request timed out');
}
}
// Different timeouts for different operations
const quickData = await api.get('/quick-data', { timeout: 5000 });
const uploadResult = await api.post('/upload', formData, { timeout: 60000 });The library is built with TypeScript and provides excellent type safety:
interface User {
id: number;
name: string;
email: string;
createdAt: string;
}
interface CreateUserRequest {
name: string;
email: string;
}
// Typed request and response
const newUser = await api.post<User>('/users', {
name: 'John Doe',
email: 'john@example.com'
} as CreateUserRequest);
// newUser is now typed as User
console.log(newUser.id, newUser.name);interface ApiErrorResponse {
code: string;
message: string;
details?: Record<string, string>;
}
try {
await api.post('/users', userData);
} catch (error) {
if (error instanceof ApiError && error.response) {
const errorData = await error.response.getData<ApiErrorResponse>();
console.log('Error code:', errorData.code);
console.log('Error message:', errorData.message);
}
}Create a typed wrapper for your API:
class MyApiClient {
private client: Client;
constructor(baseURL: string, token: string) {
this.client = new Client({
baseURL,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
}
async getUsers(): Promise<User[]> {
return this.client.get<User[]>('/users');
}
async createUser(userData: CreateUserRequest): Promise<User> {
return this.client.post<User>('/users', userData);
}
async updateUser(id: number, userData: Partial<User>): Promise<User> {
return this.client.patch<User>(`/users/${id}`, userData);
}
async deleteUser(id: number): Promise<void> {
await this.client.delete(`/users/${id}`);
}
}import React, { useEffect, useState } from 'react';
import { Client, ApiError } from '@x-common/fetch-client';
const api = new Client({
baseURL: 'https://api.example.com'
});
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUsers() {
try {
setLoading(true);
const userData = await api.get('/users');
setUsers(userData);
} catch (err) {
if (err instanceof ApiError) {
setError(`Failed to load users: ${err.message}`);
} else {
setError('An unexpected error occurred');
}
} finally {
setLoading(false);
}
}
fetchUsers();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}import { Client } from '@x-common/fetch-client';
// External API client
const externalApi = new Client({
baseURL: 'https://external-api.com',
headers: {
'X-API-Key': process.env.EXTERNAL_API_KEY
},
timeout: 10000
});
// Express route handler
app.get('/api/external-data', async (req, res) => {
try {
const data = await externalApi.get('/data', {
params: {
category: req.query.category,
limit: req.query.limit || 10
}
});
res.json(data);
} catch (error) {
if (error instanceof ApiError) {
res.status(error.status || 500).json({
error: error.message,
code: error.code
});
} else {
res.status(500).json({
error: 'Internal server error'
});
}
}
});import { Client } from '@x-common/fetch-client';
// Mock client for testing
const mockApi = new Client();
// Mock successful response
mockApi.interceptors.response.use((response) => {
if (response.request.url.includes('/users')) {
// Return mock data
const mockUsers = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
response.getData = async () => mockUsers;
}
return response;
});
// Test your code
describe('User Service', () => {
it('should fetch users', async () => {
const users = await mockApi.get('/users');
expect(users).toHaveLength(2);
expect(users[0].name).toBe('John Doe');
});
});- Node.js: 16.0.0 or higher
- TypeScript: 5.0 or higher (for development)
- Browsers: All modern browsers with fetch support
All public APIs are exported from the main entry point, allowing for clean imports:
// Import the main client
import { Client } from '@x-common/fetch-client';
// Import specific classes and utilities
import {
Client,
ApiError,
ApiRequest,
ApiResponse,
Interceptor
} from '@x-common/fetch-client';
// Import types
import type {
RequestConfig,
ClientConfig,
ResponseType,
HttpMethod
} from '@x-common/fetch-client';
// Import constants
import {
HTTP_STATUS,
ERROR_CODES,
CONTENT_TYPES
} from '@x-common/fetch-client';Client: Main HTTP client class for making requestsApiError: Enhanced error class with detailed error informationApiRequest: Request wrapper with additional metadataApiResponse: Response wrapper with parsing capabilitiesInterceptor: Middleware system for request/response processing
Body: Request body types (string, FormData, Blob, etc.)HttpMethod: HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD)ResponseType: Response parsing types (json, text, blob, arrayBuffer)RetryConfig: Retry logic configurationRequestConfig: Individual request configuration optionsClientConfig: Client-wide configuration optionsRequestOptions: Simplified request options interfaceErrorCode: Error categorization codes
HTTP_STATUS: Common HTTP status codesERROR_CODES: Standardized error codes for consistent error handlingCONTENT_TYPES: Content-type parsing patternsDEFAULT_RETRY_STATUSES: Default HTTP status codes that trigger retriesRETRY_DEFAULTS: Default retry configurationDEFAULT_RESPONSE_TYPE: Default response parsing mode
We welcome contributions! Please see our contributing guidelines for details on:
- Code style and conventions
- Testing requirements
- Pull request process
- Issue reporting
This project is licensed under the MIT License. See the LICENSE file for details.
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: API Docs