+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 119 of 355

🌐 Fetch API with TypeScript: HTTP Requests

Master the Fetch API for modern HTTP requests with type-safe implementations, error handling, and production-ready patterns 🚀

🚀Intermediate
18 min read

Prerequisites

  • Understanding of HTTP methods and status codes 📝
  • Basic knowledge of Promises and async/await ⚡
  • Experience with TypeScript interfaces and types 💻

What you'll learn

  • Master the Fetch API for HTTP requests in TypeScript 🎯
  • Implement type-safe API clients with error handling 🏗️
  • Build robust request/response patterns with interceptors 🐛
  • Create production-ready HTTP utilities and abstractions ✨

🎯 Introduction

Welcome to the modern world of HTTP requests with the Fetch API! 🌐 If XMLHttpRequest was the old rotary phone of web requests, then Fetch is the sleek smartphone - Promise-based, clean, and perfect for modern TypeScript applications!

The Fetch API provides a powerful and flexible way to make network requests. Combined with TypeScript’s type safety, you’ll build robust, maintainable HTTP clients that handle everything from simple GET requests to complex API interactions with authentication, retries, and error recovery.

By the end of this tutorial, you’ll be a master of HTTP communication, able to build type-safe API clients, handle complex request patterns, and create production-ready networking solutions. Let’s fetch some knowledge! 📡

📚 Understanding the Fetch API

🤔 What Is the Fetch API?

The Fetch API provides a modern, Promise-based interface for making HTTP requests. It’s cleaner and more powerful than XMLHttpRequest, with built-in support for streaming, CORS, and modern web standards.

// 🌟 Basic Fetch API usage
// Simple GET request
async function fetchUser(id: number): Promise<User> {
  console.log(`📡 Fetching user ${id}...`);
  
  const response = await fetch(`/api/users/${id}`);
  
  if (!response.ok) {
    throw new Error(`❌ HTTP error! status: ${response.status}`);
  }
  
  const user: User = await response.json();
  console.log('✅ User fetched successfully:', user);
  return user;
}

// 📦 Type definitions
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string;
}

// 🚀 Usage
fetchUser(123)
  .then(user => console.log('👤 User:', user))
  .catch(error => console.error('💥 Error:', error));

💡 Key Advantages

  • 🎯 Promise-Based: Clean async/await syntax
  • 🌊 Streaming Support: Handle large responses efficiently
  • 🔒 CORS-Aware: Built-in cross-origin request support
  • 📦 Request/Response Objects: Rich API for headers, body, etc.
  • 🛠️ Extensible: Easy to create abstractions and utilities
// 🎨 Understanding Request and Response objects
interface FetchOptions extends RequestInit {
  timeout?: number;
  retries?: number;
}

class TypeSafeFetch {
  private baseUrl: string;
  private defaultOptions: RequestInit;

  constructor(baseUrl: string = '', options: RequestInit = {}) {
    this.baseUrl = baseUrl;
    this.defaultOptions = {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      ...options,
    };
  }

  async request<T>(
    endpoint: string,
    options: FetchOptions = {}
  ): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`;
    const config: RequestInit = {
      ...this.defaultOptions,
      ...options,
      headers: {
        ...this.defaultOptions.headers,
        ...options.headers,
      },
    };

    console.log(`🚀 Making ${config.method || 'GET'} request to: ${url}`);

    try {
      const response = await this.fetchWithTimeout(url, config, options.timeout);
      return await this.handleResponse<T>(response);
    } catch (error) {
      console.error('💥 Request failed:', error);
      throw error;
    }
  }

  private async fetchWithTimeout(
    url: string,
    options: RequestInit,
    timeout: number = 10000
  ): Promise<Response> {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
      const response = await fetch(url, {
        ...options,
        signal: controller.signal,
      });
      clearTimeout(timeoutId);
      return response;
    } catch (error) {
      clearTimeout(timeoutId);
      if (error.name === 'AbortError') {
        throw new Error(`⏱️ Request timeout after ${timeout}ms`);
      }
      throw error;
    }
  }

  private async handleResponse<T>(response: Response): Promise<T> {
    if (!response.ok) {
      const errorData = await this.safeJsonParse(response);
      throw new HttpError(
        response.status,
        response.statusText,
        errorData
      );
    }

    const contentType = response.headers.get('content-type');
    
    if (contentType?.includes('application/json')) {
      return await response.json();
    } else if (contentType?.includes('text/')) {
      return await response.text() as unknown as T;
    } else {
      return await response.blob() as unknown as T;
    }
  }

  private async safeJsonParse(response: Response): Promise<any> {
    try {
      return await response.json();
    } catch {
      return { message: await response.text() };
    }
  }
}

// 🚨 Custom error class for HTTP errors
class HttpError extends Error {
  constructor(
    public status: number,
    public statusText: string,
    public data?: any
  ) {
    super(`HTTP ${status}: ${statusText}`);
    this.name = 'HttpError';
  }

  get isClientError(): boolean {
    return this.status >= 400 && this.status < 500;
  }

  get isServerError(): boolean {
    return this.status >= 500;
  }
}

🛠️ HTTP Methods and Request Types

📥 GET Requests - Fetching Data

GET requests are the most common HTTP method for retrieving data from servers. Let’s build type-safe GET operations:

// 🎯 Type-safe GET requests
interface ApiResponse<T> {
  data: T;
  status: 'success' | 'error';
  message?: string;
}

interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  limit: number;
  hasNext: boolean;
  hasPrev: boolean;
}

interface QueryParams {
  [key: string]: string | number | boolean | undefined;
}

class ApiClient {
  constructor(private fetcher: TypeSafeFetch) {}

  // 📋 Get single resource
  async get<T>(endpoint: string): Promise<T> {
    return this.fetcher.request<T>(endpoint, {
      method: 'GET',
    });
  }

  // 📊 Get with query parameters
  async getWithParams<T>(
    endpoint: string,
    params: QueryParams = {}
  ): Promise<T> {
    const url = this.buildUrlWithParams(endpoint, params);
    return this.fetcher.request<T>(url, {
      method: 'GET',
    });
  }

  // 📄 Get paginated data
  async getPaginated<T>(
    endpoint: string,
    page: number = 1,
    limit: number = 10,
    filters: QueryParams = {}
  ): Promise<PaginatedResponse<T>> {
    const params = {
      page,
      limit,
      ...filters,
    };

    return this.getWithParams<PaginatedResponse<T>>(endpoint, params);
  }

  private buildUrlWithParams(endpoint: string, params: QueryParams): string {
    const searchParams = new URLSearchParams();
    
    Object.entries(params).forEach(([key, value]) => {
      if (value !== undefined && value !== null) {
        searchParams.append(key, String(value));
      }
    });

    const queryString = searchParams.toString();
    return queryString ? `${endpoint}?${queryString}` : endpoint;
  }
}

// 🎮 Example usage - User management
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'moderator';
  isActive: boolean;
  lastLogin: string;
}

interface UserFilters {
  role?: string;
  isActive?: boolean;
  search?: string;
}

const fetcher = new TypeSafeFetch('/api');
const api = new ApiClient(fetcher);

// 👤 Get single user
const getUser = async (id: number): Promise<User> => {
  console.log(`🔍 Fetching user ${id}...`);
  return api.get<User>(`/users/${id}`);
};

// 👥 Get users with pagination and filters
const getUsers = async (
  page: number = 1,
  filters: UserFilters = {}
): Promise<PaginatedResponse<User>> => {
  console.log('👥 Fetching users list...');
  return api.getPaginated<User>('/users', page, 20, filters);
};

// 🔍 Search users
const searchUsers = async (query: string): Promise<User[]> => {
  console.log(`🔍 Searching users: "${query}"`);
  const response = await api.getWithParams<ApiResponse<User[]>>('/users/search', {
    q: query,
    limit: 50,
  });
  return response.data;
};

📤 POST Requests - Creating Data

POST requests are used to send data to the server to create new resources:

// 🎨 Type-safe POST requests
interface CreateUserRequest {
  name: string;
  email: string;
  password: string;
  role?: 'user' | 'moderator';
}

interface UpdateUserRequest {
  name?: string;
  email?: string;
  role?: 'admin' | 'user' | 'moderator';
  isActive?: boolean;
}

interface FileUploadOptions {
  onProgress?: (progress: number) => void;
  allowedTypes?: string[];
  maxSize?: number; // in bytes
}

class ApiClient {
  // ... previous methods

  // ✨ Create new resource
  async post<TRequest, TResponse>(
    endpoint: string,
    data: TRequest
  ): Promise<TResponse> {
    return this.fetcher.request<TResponse>(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  // 🔄 Update existing resource
  async put<TRequest, TResponse>(
    endpoint: string,
    data: TRequest
  ): Promise<TResponse> {
    return this.fetcher.request<TResponse>(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  // 🔧 Partial update
  async patch<TRequest, TResponse>(
    endpoint: string,
    data: TRequest
  ): Promise<TResponse> {
    return this.fetcher.request<TResponse>(endpoint, {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
  }

  // 🗑️ Delete resource
  async delete<TResponse = void>(endpoint: string): Promise<TResponse> {
    return this.fetcher.request<TResponse>(endpoint, {
      method: 'DELETE',
    });
  }

  // 📁 File upload with progress
  async uploadFile(
    endpoint: string,
    file: File,
    additionalData: Record<string, string> = {},
    options: FileUploadOptions = {}
  ): Promise<any> {
    // 🔍 Validate file
    this.validateFile(file, options);

    const formData = new FormData();
    formData.append('file', file);
    
    // 📝 Add additional fields
    Object.entries(additionalData).forEach(([key, value]) => {
      formData.append(key, value);
    });

    // 📊 Track upload progress
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();

      xhr.upload.addEventListener('progress', (event) => {
        if (event.lengthComputable && options.onProgress) {
          const progress = (event.loaded / event.total) * 100;
          options.onProgress(Math.round(progress));
        }
      });

      xhr.addEventListener('load', () => {
        if (xhr.status >= 200 && xhr.status < 300) {
          try {
            const response = JSON.parse(xhr.responseText);
            resolve(response);
          } catch {
            resolve(xhr.responseText);
          }
        } else {
          reject(new HttpError(xhr.status, xhr.statusText));
        }
      });

      xhr.addEventListener('error', () => {
        reject(new Error('📁 Upload failed'));
      });

      xhr.open('POST', `${this.fetcher['baseUrl']}${endpoint}`);
      xhr.send(formData);
    });
  }

  private validateFile(file: File, options: FileUploadOptions): void {
    if (options.maxSize && file.size > options.maxSize) {
      throw new Error(`📏 File too large. Max size: ${options.maxSize} bytes`);
    }

    if (options.allowedTypes && !options.allowedTypes.includes(file.type)) {
      throw new Error(`🚫 Invalid file type. Allowed: ${options.allowedTypes.join(', ')}`);
    }
  }
}

// 🎮 Example usage - User management
const createUser = async (userData: CreateUserRequest): Promise<User> => {
  console.log('👤 Creating new user...');
  
  try {
    const response = await api.post<CreateUserRequest, ApiResponse<User>>(
      '/users',
      userData
    );
    
    console.log('✅ User created successfully:', response.data);
    return response.data;
  } catch (error) {
    if (error instanceof HttpError && error.status === 409) {
      throw new Error('📧 Email already exists');
    }
    throw error;
  }
};

// 🔄 Update user
const updateUser = async (
  id: number,
  updates: UpdateUserRequest
): Promise<User> => {
  console.log(`🔄 Updating user ${id}...`);
  
  const response = await api.patch<UpdateUserRequest, ApiResponse<User>>(
    `/users/${id}`,
    updates
  );
  
  console.log('✅ User updated successfully');
  return response.data;
};

// 📁 Upload user avatar
const uploadAvatar = async (
  userId: number,
  file: File,
  onProgress?: (progress: number) => void
): Promise<string> => {
  console.log('📁 Uploading avatar...');
  
  const response = await api.uploadFile(
    `/users/${userId}/avatar`,
    file,
    { userId: userId.toString() },
    {
      allowedTypes: ['image/jpeg', 'image/png', 'image/gif'],
      maxSize: 5 * 1024 * 1024, // 5MB
      onProgress,
    }
  );
  
  console.log('✅ Avatar uploaded successfully');
  return response.avatarUrl;
};

🔧 Advanced Request Patterns

🔄 Request Interceptors and Middleware

Interceptors allow you to modify requests and responses globally:

// 🎛️ Request/Response interceptors
type RequestInterceptor = (config: RequestInit, url: string) => RequestInit | Promise<RequestInit>;
type ResponseInterceptor = (response: Response) => Response | Promise<Response>;

interface InterceptorConfig {
  onRequest?: RequestInterceptor[];
  onResponse?: ResponseInterceptor[];
  onError?: ((error: Error) => Error | Promise<Error>)[];
}

class InterceptedFetch extends TypeSafeFetch {
  private interceptors: InterceptorConfig = {
    onRequest: [],
    onResponse: [],
    onError: [],
  };

  // 🔧 Add request interceptor
  addRequestInterceptor(interceptor: RequestInterceptor): void {
    this.interceptors.onRequest?.push(interceptor);
  }

  // 📥 Add response interceptor
  addResponseInterceptor(interceptor: ResponseInterceptor): void {
    this.interceptors.onResponse?.push(interceptor);
  }

  // 🚨 Add error interceptor
  addErrorInterceptor(interceptor: (error: Error) => Error | Promise<Error>): void {
    this.interceptors.onError?.push(interceptor);
  }

  async request<T>(endpoint: string, options: FetchOptions = {}): Promise<T> {
    try {
      // 🔄 Apply request interceptors
      let config = { ...options };
      const url = `${this.baseUrl}${endpoint}`;
      
      for (const interceptor of this.interceptors.onRequest || []) {
        config = await interceptor(config, url);
      }

      // 🚀 Make the request
      let response = await this.fetchWithTimeout(url, config, options.timeout);

      // 📥 Apply response interceptors
      for (const interceptor of this.interceptors.onResponse || []) {
        response = await interceptor(response);
      }

      return await this.handleResponse<T>(response);
    } catch (error) {
      // 🚨 Apply error interceptors
      let processedError = error as Error;
      
      for (const interceptor of this.interceptors.onError || []) {
        processedError = await interceptor(processedError);
      }
      
      throw processedError;
    }
  }
}

// 🔑 Authentication interceptor
const authInterceptor: RequestInterceptor = (config, url) => {
  const token = localStorage.getItem('authToken');
  
  if (token && !url.includes('/auth/')) {
    return {
      ...config,
      headers: {
        ...config.headers,
        'Authorization': `Bearer ${token}`,
      },
    };
  }
  
  return config;
};

// 📊 Logging interceptor
const loggingInterceptor: RequestInterceptor = (config, url) => {
  console.log(`🚀 ${config.method || 'GET'} ${url}`, {
    headers: config.headers,
    body: config.body,
  });
  return config;
};

// 🔄 Token refresh interceptor
const tokenRefreshInterceptor: ResponseInterceptor = async (response) => {
  if (response.status === 401) {
    console.log('🔄 Attempting to refresh token...');
    
    try {
      const refreshToken = localStorage.getItem('refreshToken');
      const refreshResponse = await fetch('/auth/refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refreshToken }),
      });
      
      if (refreshResponse.ok) {
        const { accessToken } = await refreshResponse.json();
        localStorage.setItem('authToken', accessToken);
        console.log('✅ Token refreshed successfully');
      }
    } catch (error) {
      console.error('❌ Token refresh failed:', error);
      // Redirect to login or handle auth failure
      window.location.href = '/login';
    }
  }
  
  return response;
};

// 🛠️ Setup interceptors
const apiClient = new InterceptedFetch('/api');
apiClient.addRequestInterceptor(authInterceptor);
apiClient.addRequestInterceptor(loggingInterceptor);
apiClient.addResponseInterceptor(tokenRefreshInterceptor);

🔄 Retry Logic and Circuit Breaker

Implement robust retry mechanisms for handling transient failures:

// 🔄 Advanced retry logic with exponential backoff
interface RetryConfig {
  maxAttempts: number;
  baseDelay: number;
  maxDelay: number;
  backoffFactor: number;
  retryCondition?: (error: Error) => boolean;
}

interface CircuitBreakerConfig {
  failureThreshold: number;
  recoveryTimeout: number;
  monitoringPeriod: number;
}

enum CircuitState {
  CLOSED = 'closed',
  OPEN = 'open',
  HALF_OPEN = 'half_open',
}

class CircuitBreaker {
  private state = CircuitState.CLOSED;
  private failureCount = 0;
  private lastFailureTime = 0;
  private successCount = 0;

  constructor(private config: CircuitBreakerConfig) {}

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === CircuitState.OPEN) {
      if (this.shouldAttemptReset()) {
        this.state = CircuitState.HALF_OPEN;
        console.log('🔄 Circuit breaker transitioning to HALF_OPEN');
      } else {
        throw new Error('🚫 Circuit breaker is OPEN');
      }
    }

    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private shouldAttemptReset(): boolean {
    return Date.now() - this.lastFailureTime >= this.config.recoveryTimeout;
  }

  private onSuccess(): void {
    this.failureCount = 0;
    
    if (this.state === CircuitState.HALF_OPEN) {
      this.successCount++;
      if (this.successCount >= 3) { // Require 3 successes to close
        this.state = CircuitState.CLOSED;
        this.successCount = 0;
        console.log('✅ Circuit breaker CLOSED');
      }
    }
  }

  private onFailure(): void {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    
    if (this.failureCount >= this.config.failureThreshold) {
      this.state = CircuitState.OPEN;
      console.log('🚫 Circuit breaker OPENED');
    }
  }

  get currentState(): CircuitState {
    return this.state;
  }
}

class ResilientFetch extends InterceptedFetch {
  private circuitBreaker: CircuitBreaker;

  constructor(
    baseUrl: string = '',
    options: RequestInit = {},
    circuitBreakerConfig: CircuitBreakerConfig = {
      failureThreshold: 5,
      recoveryTimeout: 30000,
      monitoringPeriod: 60000,
    }
  ) {
    super(baseUrl, options);
    this.circuitBreaker = new CircuitBreaker(circuitBreakerConfig);
  }

  async requestWithRetry<T>(
    endpoint: string,
    options: FetchOptions & { retry?: RetryConfig } = {}
  ): Promise<T> {
    const retryConfig: RetryConfig = {
      maxAttempts: 3,
      baseDelay: 1000,
      maxDelay: 10000,
      backoffFactor: 2,
      retryCondition: (error) => {
        if (error instanceof HttpError) {
          // Retry on 5xx server errors and specific 4xx errors
          return error.isServerError || error.status === 429; // Rate limited
        }
        return true; // Retry network errors
      },
      ...options.retry,
    };

    return this.circuitBreaker.execute(async () => {
      return this.retryOperation(
        () => super.request<T>(endpoint, options),
        retryConfig
      );
    });
  }

  private async retryOperation<T>(
    operation: () => Promise<T>,
    config: RetryConfig
  ): Promise<T> {
    let lastError: Error;

    for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
      try {
        console.log(`🔄 Attempt ${attempt}/${config.maxAttempts}`);
        return await operation();
      } catch (error) {
        lastError = error as Error;
        
        if (
          attempt === config.maxAttempts ||
          !config.retryCondition?.(lastError)
        ) {
          throw lastError;
        }

        const delay = Math.min(
          config.baseDelay * Math.pow(config.backoffFactor, attempt - 1),
          config.maxDelay
        );
        
        console.log(`⏱️ Retrying in ${delay}ms... (${lastError.message})`);
        await this.sleep(delay);
      }
    }

    throw lastError!;
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// 🎮 Usage example with retry and circuit breaker
const resilientApi = new ResilientFetch('/api');

const getUserWithRetry = async (id: number): Promise<User> => {
  return resilientApi.requestWithRetry<User>(`/users/${id}`, {
    method: 'GET',
    retry: {
      maxAttempts: 5,
      baseDelay: 500,
      retryCondition: (error) => {
        // Don't retry on 404 or 403 errors
        if (error instanceof HttpError) {
          return ![404, 403].includes(error.status);
        }
        return true;
      },
    },
  });
};

🎯 Type-Safe API Client Patterns

🏗️ Generic API Resource Client

Create a generic client that works with any REST resource:

// 🎨 Generic resource client
interface ResourceClient<T, TCreate = Omit<T, 'id'>, TUpdate = Partial<TCreate>> {
  getAll(params?: QueryParams): Promise<PaginatedResponse<T>>;
  getById(id: string | number): Promise<T>;
  create(data: TCreate): Promise<T>;
  update(id: string | number, data: TUpdate): Promise<T>;
  delete(id: string | number): Promise<void>;
}

class BaseResourceClient<T, TCreate = Omit<T, 'id'>, TUpdate = Partial<TCreate>>
  implements ResourceClient<T, TCreate, TUpdate> {
  
  constructor(
    private client: ResilientFetch,
    private endpoint: string
  ) {}

  async getAll(params: QueryParams = {}): Promise<PaginatedResponse<T>> {
    const url = this.buildUrlWithParams(this.endpoint, params);
    return this.client.requestWithRetry<PaginatedResponse<T>>(url);
  }

  async getById(id: string | number): Promise<T> {
    return this.client.requestWithRetry<T>(`${this.endpoint}/${id}`);
  }

  async create(data: TCreate): Promise<T> {
    return this.client.requestWithRetry<T>(this.endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async update(id: string | number, data: TUpdate): Promise<T> {
    return this.client.requestWithRetry<T>(`${this.endpoint}/${id}`, {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
  }

  async delete(id: string | number): Promise<void> {
    await this.client.requestWithRetry<void>(`${this.endpoint}/${id}`, {
      method: 'DELETE',
    });
  }

  private buildUrlWithParams(endpoint: string, params: QueryParams): string {
    const searchParams = new URLSearchParams();
    
    Object.entries(params).forEach(([key, value]) => {
      if (value !== undefined && value !== null) {
        searchParams.append(key, String(value));
      }
    });

    const queryString = searchParams.toString();
    return queryString ? `${endpoint}?${queryString}` : endpoint;
  }
}

// 🎯 Specific resource clients
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'moderator';
  isActive: boolean;
  createdAt: string;
  updatedAt: string;
}

interface CreateUser {
  name: string;
  email: string;
  password: string;
  role?: 'user' | 'moderator';
}

interface UpdateUser {
  name?: string;
  email?: string;
  role?: 'admin' | 'user' | 'moderator';
  isActive?: boolean;
}

class UserClient extends BaseResourceClient<User, CreateUser, UpdateUser> {
  constructor(client: ResilientFetch) {
    super(client, '/users');
  }

  // 🔍 Additional user-specific methods
  async getByEmail(email: string): Promise<User | null> {
    try {
      return await this.client.requestWithRetry<User>(`/users/by-email/${email}`);
    } catch (error) {
      if (error instanceof HttpError && error.status === 404) {
        return null;
      }
      throw error;
    }
  }

  async changePassword(id: number, newPassword: string): Promise<void> {
    await this.client.requestWithRetry<void>(`/users/${id}/password`, {
      method: 'POST',
      body: JSON.stringify({ password: newPassword }),
    });
  }

  async toggleStatus(id: number): Promise<User> {
    return this.client.requestWithRetry<User>(`/users/${id}/toggle-status`, {
      method: 'POST',
    });
  }

  async searchUsers(query: string, limit: number = 20): Promise<User[]> {
    const response = await this.getAll({ 
      search: query, 
      limit,
    });
    return response.items;
  }
}

// 📝 Posts resource
interface Post {
  id: number;
  title: string;
  content: string;
  authorId: number;
  published: boolean;
  tags: string[];
  createdAt: string;
  updatedAt: string;
}

interface CreatePost {
  title: string;
  content: string;
  authorId: number;
  tags?: string[];
}

class PostClient extends BaseResourceClient<Post, CreatePost> {
  constructor(client: ResilientFetch) {
    super(client, '/posts');
  }

  async getByAuthor(authorId: number): Promise<Post[]> {
    const response = await this.getAll({ authorId });
    return response.items;
  }

  async publish(id: number): Promise<Post> {
    return this.client.requestWithRetry<Post>(`/posts/${id}/publish`, {
      method: 'POST',
    });
  }

  async getByTags(tags: string[]): Promise<Post[]> {
    const response = await this.getAll({ 
      tags: tags.join(','),
    });
    return response.items;
  }
}

// 🏗️ API service composition
class ApiService {
  private client: ResilientFetch;
  
  public users: UserClient;
  public posts: PostClient;

  constructor(baseUrl: string) {
    this.client = new ResilientFetch(baseUrl);
    
    // 🔧 Setup common interceptors
    this.setupInterceptors();
    
    // 🎯 Initialize resource clients
    this.users = new UserClient(this.client);
    this.posts = new PostClient(this.client);
  }

  private setupInterceptors(): void {
    // 🔑 Auth interceptor
    this.client.addRequestInterceptor((config, url) => {
      const token = localStorage.getItem('authToken');
      if (token) {
        return {
          ...config,
          headers: {
            ...config.headers,
            'Authorization': `Bearer ${token}`,
          },
        };
      }
      return config;
    });

    // 📊 Request ID for tracing
    this.client.addRequestInterceptor((config) => {
      const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
      return {
        ...config,
        headers: {
          ...config.headers,
          'X-Request-ID': requestId,
        },
      };
    });
  }
}

// 🚀 Usage
const api = new ApiService('https://api.example.com');

// 👤 User operations
const createNewUser = async (): Promise<void> => {
  try {
    const user = await api.users.create({
      name: 'John Doe',
      email: '[email protected]',
      password: 'securePassword123',
      role: 'user',
    });
    
    console.log('✅ User created:', user);
    
    // 📝 Create a post for the user
    const post = await api.posts.create({
      title: 'My First Post',
      content: 'Hello, World!',
      authorId: user.id,
      tags: ['intro', 'typescript'],
    });
    
    console.log('✅ Post created:', post);
  } catch (error) {
    console.error('💥 Operation failed:', error);
  }
};

🧪 Testing HTTP Clients

🎭 Mocking Fetch for Tests

Create comprehensive tests for your HTTP clients:

// 🧪 Test utilities for Fetch API
interface MockResponse {
  ok: boolean;
  status: number;
  statusText: string;
  json: () => Promise<any>;
  text: () => Promise<string>;
  blob: () => Promise<Blob>;
  headers: Headers;
}

class FetchMock {
  private static originalFetch: typeof global.fetch;
  private static mockResponses: Map<string, MockResponse[]> = new Map();

  static setup(): void {
    this.originalFetch = global.fetch;
    global.fetch = jest.fn(this.mockImplementation.bind(this));
  }

  static teardown(): void {
    global.fetch = this.originalFetch;
    this.mockResponses.clear();
  }

  static mockResponse(
    url: string | RegExp,
    response: Partial<MockResponse>,
    options: { once?: boolean; method?: string } = {}
  ): void {
    const mockResponse: MockResponse = {
      ok: response.ok ?? true,
      status: response.status ?? 200,
      statusText: response.statusText ?? 'OK',
      json: response.json ?? (() => Promise.resolve({})),
      text: response.text ?? (() => Promise.resolve('')),
      blob: response.blob ?? (() => Promise.resolve(new Blob())),
      headers: response.headers ?? new Headers(),
    };

    const key = url instanceof RegExp ? url.source : url;
    
    if (!this.mockResponses.has(key)) {
      this.mockResponses.set(key, []);
    }
    
    this.mockResponses.get(key)!.push(mockResponse);
  }

  private static async mockImplementation(
    input: RequestInfo | URL,
    init?: RequestInit
  ): Promise<Response> {
    const url = input instanceof Request ? input.url : input.toString();
    const method = init?.method ?? 'GET';

    // 🔍 Find matching mock
    for (const [pattern, responses] of this.mockResponses.entries()) {
      const isMatch = pattern instanceof RegExp 
        ? new RegExp(pattern).test(url)
        : url.includes(pattern);

      if (isMatch && responses.length > 0) {
        const response = responses.shift()!;
        return response as unknown as Response;
      }
    }

    throw new Error(`🚫 No mock found for ${method} ${url}`);
  }

  static getRequestHistory(): Array<{ url: string; options?: RequestInit }> {
    return (global.fetch as jest.Mock).mock.calls.map(([url, options]) => ({
      url: url instanceof Request ? url.url : url.toString(),
      options,
    }));
  }
}

// 🧪 Test suite
describe('ApiService', () => {
  let api: ApiService;

  beforeEach(() => {
    FetchMock.setup();
    api = new ApiService('https://api.test.com');
  });

  afterEach(() => {
    FetchMock.teardown();
  });

  describe('UserClient', () => {
    it('should fetch user by ID', async () => {
      // 🎭 Setup mock
      const mockUser: User = {
        id: 1,
        name: 'John Doe',
        email: '[email protected]',
        role: 'user',
        isActive: true,
        createdAt: '2023-01-01T00:00:00Z',
        updatedAt: '2023-01-01T00:00:00Z',
      };

      FetchMock.mockResponse('/users/1', {
        ok: true,
        status: 200,
        json: () => Promise.resolve(mockUser),
      });

      // 🎯 Execute test
      const user = await api.users.getById(1);

      // ✅ Assertions
      expect(user).toEqual(mockUser);
      
      const requests = FetchMock.getRequestHistory();
      expect(requests).toHaveLength(1);
      expect(requests[0].url).toContain('/users/1');
    });

    it('should handle user not found', async () => {
      // 🎭 Setup mock for 404
      FetchMock.mockResponse('/users/999', {
        ok: false,
        status: 404,
        statusText: 'Not Found',
        json: () => Promise.resolve({ message: 'User not found' }),
      });

      // 🎯 Execute and expect error
      await expect(api.users.getById(999)).rejects.toThrow('HTTP 404: Not Found');
    });

    it('should create user successfully', async () => {
      // 🎭 Setup mocks
      const createData: CreateUser = {
        name: 'Jane Doe',
        email: '[email protected]',
        password: 'password123',
        role: 'user',
      };

      const mockCreatedUser: User = {
        id: 2,
        ...createData,
        isActive: true,
        createdAt: '2023-01-01T00:00:00Z',
        updatedAt: '2023-01-01T00:00:00Z',
      };

      FetchMock.mockResponse('/users', {
        ok: true,
        status: 201,
        json: () => Promise.resolve(mockCreatedUser),
      });

      // 🎯 Execute test
      const user = await api.users.create(createData);

      // ✅ Assertions
      expect(user).toEqual(mockCreatedUser);
      
      const requests = FetchMock.getRequestHistory();
      expect(requests[0].options?.method).toBe('POST');
      expect(requests[0].options?.body).toBe(JSON.stringify(createData));
    });

    it('should retry on server error', async () => {
      // 🎭 Setup mocks - first fails, second succeeds
      FetchMock.mockResponse('/users/1', {
        ok: false,
        status: 500,
        statusText: 'Internal Server Error',
      });

      FetchMock.mockResponse('/users/1', {
        ok: true,
        status: 200,
        json: () => Promise.resolve({ id: 1, name: 'John' }),
      });

      // 🎯 Execute test
      const user = await api.users.getById(1);

      // ✅ Assertions
      expect(user.id).toBe(1);
      expect(FetchMock.getRequestHistory()).toHaveLength(2);
    });
  });

  describe('Error Handling', () => {
    it('should handle network errors', async () => {
      // 🎭 Mock network failure
      FetchMock.mockResponse('/users/1', {
        ok: false,
        status: 0,
        statusText: 'Network Error',
      });

      // 🎯 Execute and expect error
      await expect(api.users.getById(1)).rejects.toThrow();
    });

    it('should handle timeout', async () => {
      // 🎭 Mock slow response
      FetchMock.mockResponse('/users/1', {
        ok: true,
        status: 200,
        json: () => new Promise(resolve => 
          setTimeout(() => resolve({}), 15000)
        ),
      });

      // 🎯 Execute with short timeout
      await expect(
        api.users.getById(1)
      ).rejects.toThrow('Request timeout');
    }, 20000);
  });
});

// 🎭 Integration test utilities
class TestServer {
  private handlers: Map<string, (req: any) => any> = new Map();

  register(method: string, path: string, handler: (req: any) => any): void {
    this.handlers.set(`${method}:${path}`, handler);
  }

  async handleRequest(method: string, path: string, body?: any): Promise<any> {
    const handler = this.handlers.get(`${method}:${path}`);
    
    if (!handler) {
      throw new Error(`No handler for ${method} ${path}`);
    }

    return handler({ method, path, body });
  }
}

// 🧪 Integration test example
describe('User Management Integration', () => {
  let server: TestServer;
  let api: ApiService;

  beforeEach(() => {
    server = new TestServer();
    api = new ApiService('http://localhost:3000');

    // 🎭 Setup test server handlers
    server.register('GET', '/users/1', () => ({
      id: 1,
      name: 'Test User',
      email: '[email protected]',
      role: 'user',
      isActive: true,
      createdAt: '2023-01-01T00:00:00Z',
      updatedAt: '2023-01-01T00:00:00Z',
    }));

    server.register('POST', '/users', (req) => ({
      id: 2,
      ...req.body,
      isActive: true,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    }));
  });

  it('should perform complete user lifecycle', async () => {
    // 🎯 Create user
    const createData: CreateUser = {
      name: 'Integration Test User',
      email: '[email protected]',
      password: 'testpass123',
    };

    const createdUser = await api.users.create(createData);
    expect(createdUser.name).toBe(createData.name);

    // 🔄 Update user
    const updatedUser = await api.users.update(createdUser.id, {
      name: 'Updated Name',
    });
    expect(updatedUser.name).toBe('Updated Name');

    // 🔍 Fetch user
    const fetchedUser = await api.users.getById(createdUser.id);
    expect(fetchedUser.id).toBe(createdUser.id);
  });
});

🎉 Practical Examples and Use Cases

🛒 E-commerce API Client

A real-world example of a complete e-commerce API client:

// 🛒 E-commerce API types
interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  currency: string;
  category: string;
  inStock: boolean;
  imageUrls: string[];
  tags: string[];
}

interface CartItem {
  productId: string;
  quantity: number;
  price: number;
}

interface Cart {
  id: string;
  userId: string;
  items: CartItem[];
  total: number;
  currency: string;
  createdAt: string;
  updatedAt: string;
}

interface Order {
  id: string;
  userId: string;
  items: CartItem[];
  total: number;
  status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
  shippingAddress: Address;
  paymentMethod: PaymentMethod;
  createdAt: string;
}

interface Address {
  street: string;
  city: string;
  state: string;
  zipCode: string;
  country: string;
}

interface PaymentMethod {
  type: 'credit_card' | 'paypal' | 'apple_pay';
  last4?: string;
  expiryMonth?: number;
  expiryYear?: number;
}

// 🏪 E-commerce API client
class ECommerceApi {
  private client: ResilientFetch;

  constructor(baseUrl: string) {
    this.client = new ResilientFetch(baseUrl);
    this.setupInterceptors();
  }

  // 🛍️ Product operations
  async getProducts(filters: {
    category?: string;
    minPrice?: number;
    maxPrice?: number;
    inStock?: boolean;
    search?: string;
    page?: number;
    limit?: number;
  } = {}): Promise<PaginatedResponse<Product>> {
    return this.client.requestWithRetry<PaginatedResponse<Product>>('/products', {
      method: 'GET',
      retry: { maxAttempts: 3 },
    });
  }

  async getProduct(id: string): Promise<Product> {
    return this.client.requestWithRetry<Product>(`/products/${id}`);
  }

  async searchProducts(query: string): Promise<Product[]> {
    const response = await this.client.requestWithRetry<PaginatedResponse<Product>>(
      `/products/search?q=${encodeURIComponent(query)}`
    );
    return response.items;
  }

  // 🛒 Cart operations
  async getCart(userId: string): Promise<Cart> {
    return this.client.requestWithRetry<Cart>(`/users/${userId}/cart`);
  }

  async addToCart(userId: string, productId: string, quantity: number): Promise<Cart> {
    return this.client.requestWithRetry<Cart>(`/users/${userId}/cart/items`, {
      method: 'POST',
      body: JSON.stringify({ productId, quantity }),
    });
  }

  async updateCartItem(
    userId: string,
    productId: string,
    quantity: number
  ): Promise<Cart> {
    return this.client.requestWithRetry<Cart>(
      `/users/${userId}/cart/items/${productId}`,
      {
        method: 'PUT',
        body: JSON.stringify({ quantity }),
      }
    );
  }

  async removeFromCart(userId: string, productId: string): Promise<Cart> {
    return this.client.requestWithRetry<Cart>(
      `/users/${userId}/cart/items/${productId}`,
      { method: 'DELETE' }
    );
  }

  async clearCart(userId: string): Promise<void> {
    await this.client.requestWithRetry<void>(`/users/${userId}/cart`, {
      method: 'DELETE',
    });
  }

  // 📦 Order operations
  async createOrder(
    userId: string,
    shippingAddress: Address,
    paymentMethod: PaymentMethod
  ): Promise<Order> {
    return this.client.requestWithRetry<Order>(`/users/${userId}/orders`, {
      method: 'POST',
      body: JSON.stringify({ shippingAddress, paymentMethod }),
    });
  }

  async getOrders(userId: string): Promise<Order[]> {
    const response = await this.client.requestWithRetry<PaginatedResponse<Order>>(
      `/users/${userId}/orders`
    );
    return response.items;
  }

  async getOrder(userId: string, orderId: string): Promise<Order> {
    return this.client.requestWithRetry<Order>(`/users/${userId}/orders/${orderId}`);
  }

  async cancelOrder(userId: string, orderId: string): Promise<Order> {
    return this.client.requestWithRetry<Order>(
      `/users/${userId}/orders/${orderId}/cancel`,
      { method: 'POST' }
    );
  }

  // 💳 Payment operations
  async processPayment(
    orderId: string,
    paymentDetails: {
      amount: number;
      currency: string;
      paymentMethodId: string;
    }
  ): Promise<{ success: boolean; transactionId?: string; error?: string }> {
    return this.client.requestWithRetry(
      `/orders/${orderId}/payment`,
      {
        method: 'POST',
        body: JSON.stringify(paymentDetails),
        timeout: 30000, // Longer timeout for payment processing
      }
    );
  }

  private setupInterceptors(): void {
    // 🔑 Authentication
    this.client.addRequestInterceptor((config) => {
      const token = localStorage.getItem('authToken');
      if (token) {
        return {
          ...config,
          headers: {
            ...config.headers,
            'Authorization': `Bearer ${token}`,
          },
        };
      }
      return config;
    });

    // 📊 Request tracking
    this.client.addRequestInterceptor((config, url) => {
      console.log(`🛒 E-commerce API: ${config.method || 'GET'} ${url}`);
      return config;
    });

    // 🚨 Error handling
    this.client.addErrorInterceptor((error) => {
      if (error instanceof HttpError) {
        switch (error.status) {
          case 401:
            // Redirect to login
            window.location.href = '/login';
            break;
          case 402:
            // Payment required
            console.error('💳 Payment required:', error.data);
            break;
          case 409:
            // Conflict - item out of stock
            console.error('📦 Item conflict:', error.data);
            break;
        }
      }
      return error;
    });
  }
}

// 🎮 Shopping cart manager
class ShoppingCartManager {
  private cartCache: Cart | null = null;
  private syncPending = false;

  constructor(
    private api: ECommerceApi,
    private userId: string
  ) {}

  async getCart(): Promise<Cart> {
    if (this.cartCache && !this.syncPending) {
      return this.cartCache;
    }

    try {
      this.cartCache = await this.api.getCart(this.userId);
      this.syncPending = false;
      return this.cartCache;
    } catch (error) {
      console.error('🛒 Failed to fetch cart:', error);
      throw error;
    }
  }

  async addItem(productId: string, quantity: number = 1): Promise<Cart> {
    try {
      console.log(`➕ Adding ${quantity}x ${productId} to cart`);
      
      this.cartCache = await this.api.addToCart(this.userId, productId, quantity);
      this.syncPending = false;
      
      // 🎉 Show success notification
      this.showNotification('✅ Item added to cart!');
      
      return this.cartCache;
    } catch (error) {
      console.error('❌ Failed to add item to cart:', error);
      throw error;
    }
  }

  async updateQuantity(productId: string, quantity: number): Promise<Cart> {
    if (quantity <= 0) {
      return this.removeItem(productId);
    }

    try {
      this.cartCache = await this.api.updateCartItem(this.userId, productId, quantity);
      this.syncPending = false;
      return this.cartCache;
    } catch (error) {
      console.error('🔄 Failed to update cart item:', error);
      throw error;
    }
  }

  async removeItem(productId: string): Promise<Cart> {
    try {
      console.log(`🗑️ Removing ${productId} from cart`);
      
      this.cartCache = await this.api.removeFromCart(this.userId, productId);
      this.syncPending = false;
      
      this.showNotification('🗑️ Item removed from cart');
      
      return this.cartCache;
    } catch (error) {
      console.error('❌ Failed to remove item from cart:', error);
      throw error;
    }
  }

  async checkout(
    shippingAddress: Address,
    paymentMethod: PaymentMethod
  ): Promise<Order> {
    try {
      console.log('💳 Processing checkout...');
      
      const order = await this.api.createOrder(
        this.userId,
        shippingAddress,
        paymentMethod
      );
      
      // 🎉 Clear cart after successful order
      this.cartCache = null;
      await this.api.clearCart(this.userId);
      
      this.showNotification('🎉 Order placed successfully!');
      
      return order;
    } catch (error) {
      console.error('💥 Checkout failed:', error);
      throw error;
    }
  }

  private showNotification(message: string): void {
    // Implementation depends on your notification system
    console.log(`🔔 ${message}`);
  }

  get itemCount(): number {
    return this.cartCache?.items.reduce((sum, item) => sum + item.quantity, 0) || 0;
  }

  get total(): number {
    return this.cartCache?.total || 0;
  }
}

// 🎯 Usage example
const ecommerceApi = new ECommerceApi('https://api.mystore.com');
const cartManager = new ShoppingCartManager(ecommerceApi, 'user_123');

// 🛍️ Shopping flow
const shopExample = async (): Promise<void> => {
  try {
    // 🔍 Search for products
    const products = await ecommerceApi.searchProducts('laptop');
    console.log(`🔍 Found ${products.length} laptops`);

    // ➕ Add product to cart
    const cart = await cartManager.addItem(products[0].id, 1);
    console.log(`🛒 Cart now has ${cartManager.itemCount} items`);

    // 💳 Checkout
    const order = await cartManager.checkout(
      {
        street: '123 Main St',
        city: 'Anytown',
        state: 'CA',
        zipCode: '12345',
        country: 'US',
      },
      {
        type: 'credit_card',
        last4: '1234',
        expiryMonth: 12,
        expiryYear: 2025,
      }
    );

    console.log(`🎉 Order ${order.id} placed successfully!`);
  } catch (error) {
    console.error('💥 Shopping failed:', error);
  }
};

🎯 Conclusion

Congratulations! 🎉 You’ve mastered the Fetch API with TypeScript and built production-ready HTTP clients! You now have the skills to:

  • 🌐 Master HTTP requests with type-safe Fetch API patterns
  • 🔧 Build robust clients with retry logic, circuit breakers, and error handling
  • 🎯 Create reusable abstractions with generic resource clients and interceptors
  • 🧪 Test HTTP code with comprehensive mocking and integration tests

The Fetch API combined with TypeScript’s type safety provides everything you need to build reliable, maintainable networking code. From simple GET requests to complex e-commerce systems, you’re now equipped to handle any HTTP communication challenge!

Keep practicing with different API patterns, explore GraphQL clients, and remember that great networking code is the foundation of exceptional user experiences. The web is your oyster! 🌐✨