Skip to content

x-common/fetch-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fetch API - TypeScript HTTP Client

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.

Table of Contents

Features

  • 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

Installation

npm install fetch-api
yarn add fetch-api
pnpm add fetch-api

Quick Start

Basic Usage

import { ApiClient } from 'fetch-api';

// Create a client instance
const api = new ApiClient({
  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'
});

With Authentication

import { ApiClient } from 'fetch-api';

const api = new ApiClient({
  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');

API Reference

Client Configuration

The ApiClient 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
}

Configuration Examples

// Minimal configuration
const api = new ApiClient();

// Basic configuration
const api = new ApiClient({
  baseURL: 'https://api.example.com'
});

// Full configuration
const api = new ApiClient({
  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'
});

HTTP Methods

The client supports all standard HTTP methods:

GET Requests

// 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 Requests

// 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' }
});

PUT Requests

// Update entire resource
const updatedUser = await api.put('/users/123', {
  name: 'Jane Doe',
  email: 'jane@example.com',
  status: 'active'
});

PATCH Requests

// Partial update
const updatedUser = await api.patch('/users/123', {
  status: 'inactive'
});

DELETE Requests

// Delete resource
await api.delete('/users/123');

// Delete with confirmation
await api.delete('/users/123', {
  headers: { 'X-Confirm': 'true' }
});

HEAD Requests

// Get headers only
const headers = await api.head('/users/123');

Request Configuration

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
}

Response Handling

Automatic Response Parsing

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 Blob

Manual Response Type

You 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' });

Working with Raw Responses

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();

Error Handling

The client provides comprehensive error handling with detailed error information:

import { ApiError, ERROR_CODES } from 'fetch-api';

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;
    }
  }
}

Error Types

  • NETWORK_ERROR: Network connection issues
  • TIMEOUT_ERROR: Request exceeded timeout limit
  • HTTP_ERROR: Server returned error status (4xx, 5xx)
  • PARSE_ERROR: Failed to parse response data
  • VALIDATION_ERROR: Request validation failed
  • ABORT_ERROR: Request was cancelled
  • UNKNOWN_ERROR: Unexpected error occurred

Interceptors

Interceptors allow you to transform requests and responses globally:

Request Interceptors

// 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;
});

Response Interceptors

// 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;
});

Retry Logic

Configure automatic retries for failed requests:

Basic Retry Configuration

const api = new ApiClient({
  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
  }
});

Advanced Retry Configuration

const api = new ApiClient({
  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;
    }
  }
});

Per-Request Retry

// Override retry config for specific requests
const data = await api.get('/critical-data', {
  retry: {
    maxRetries: 10,
    delay: 5000
  }
});

Advanced Usage

Multiple Client Instances

Create different client instances for different APIs:

// Main API client
const mainApi = new ApiClient({
  baseURL: 'https://api.myapp.com',
  headers: { 'Authorization': 'Bearer main-token' }
});

// Analytics API client
const analyticsApi = new ApiClient({
  baseURL: 'https://analytics.myapp.com',
  headers: { 'X-API-Key': 'analytics-key' },
  timeout: 5000
});

// File upload client
const uploadApi = new ApiClient({
  baseURL: 'https://upload.myapp.com',
  timeout: 60000, // Longer timeout for uploads
  retry: { maxRetries: 1 } // Fewer retries for uploads
});

Dynamic Configuration

Update client configuration at runtime:

const api = new ApiClient();

// 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);

File Uploads

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);

Timeout Configuration

Manage request timeouts to prevent long-running requests:

// Global timeout configuration
const api = new ApiClient({
  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 });

TypeScript Support

The library is built with TypeScript and provides excellent type safety:

Typed Responses

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);

Custom Error Types

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);
  }
}

Generic Client

Create a typed wrapper for your API:

class MyApiClient {
  private client: ApiClient;

  constructor(baseURL: string, token: string) {
    this.client = new ApiClient({
      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}`);
  }
}

Examples

React Integration

import React, { useEffect, useState } from 'react';
import { ApiClient, ApiError } from 'fetch-api';

const api = new ApiClient({
  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>
  );
}

Node.js Server Integration

import { ApiClient } from 'fetch-api';

// External API client
const externalApi = new ApiClient({
  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'
      });
    }
  }
});

Testing with Mock Responses

import { ApiClient } from 'fetch-api';

// Mock client for testing
const mockApi = new ApiClient();

// 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');
  });
});

Development

Building the Project

# Install dependencies
npm install

# Build the project
npm run build

# Watch mode for development
npm run dev

# Clean build artifacts
npm run clean

Documentation Generation

# Generate markdown documentation
npm run docs

# Generate HTML documentation
npm run docs:html

# Generate both and serve locally
npm run docs:build
npm run docs:serve

Publishing

# Build and publish to npm
npm run prepublishOnly
npm publish

Browser Support

This library works in all modern browsers that support:

  • Fetch API (native or polyfilled)
  • Promises (native or polyfilled)
  • ES6+ features (or transpiled)

For older browser support, include appropriate polyfills:

<!-- For IE11 and older browsers -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=fetch,Promise"></script>

Requirements

  • Node.js: 16.0.0 or higher
  • TypeScript: 5.0 or higher (for development)
  • Browsers: All modern browsers with fetch support

Contributing

We welcome contributions! Please see our contributing guidelines for details on:

  • Code style and conventions
  • Testing requirements
  • Pull request process
  • Issue reporting

License

This project is licensed under the MIT License. See the LICENSE file for details.

Changelog

See CHANGELOG.md for version history and breaking changes.

Support


Built with TypeScript and modern web standards. Zero dependencies, maximum performance.

About

fetch-api

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published