Advanced HTTP Client in TypeScript: From Basic Requests to Production Solutions
When developing modern web applications in TypeScript, working with HTTP requests goes far beyond a simple fetch call. At an advanced level, you need not only to send requests, but also to properly handle errors, manage timeouts, type responses, implement interceptors, and even cache data. In this article, we will look at how to build a reliable and flexible HTTP client in TypeScript suitable for production projects.
1. Creating a Typed HTTP Client Using Generics
One of the main advantages of TypeScript is strict typing. Let's create a base HttpClient class that will accept the response type via generics. This will help avoid errors at compile time.
// types.tsexport interface ApiResponse<T> { data: T; status: number; ok: boolean;}
export interface RequestConfig { headers?: Record<string, string>; params?: Record<string, string>; signal?: AbortSignal;}
// http-client.tsimport { ApiResponse, RequestConfig } from './types';
class HttpClient { private baseUrl: string;
constructor(baseUrl: string) { this.baseUrl = baseUrl; }
async get<T>(endpoint: string, config?: RequestConfig): Promise<ApiResponse<T>> { const url = new URL(`${this.baseUrl}${endpoint}`); if (config?.params) { Object.entries(config.params).forEach(([key, value]) => { url.searchParams.append(key, value); }); }
const response = await fetch(url.toString(), { method: 'GET', headers: { 'Content-Type': 'application/json', ...config?.headers, }, signal: config?.signal, });
const data: T = await response.json(); return { data, status: response.status, ok: response.ok, }; }
async post<T, U>(endpoint: string, body: U, config?: RequestConfig): Promise<ApiResponse<T>> { const response = await fetch(`${this.baseUrl}${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...config?.headers, }, body: JSON.stringify(body), signal: config?.signal, });
const data: T = await response.json(); return { data, status: response.status, ok: response.ok }; }}
export default HttpClient;2. Error Handling and Timeouts
An advanced client should correctly handle both network errors and business logic errors. Let's add a timeout mechanism using AbortController and custom error handling.
// errors.tsexport class HttpError extends Error { status: number; data?: unknown;
constructor(message: string, status: number, data?: unknown) { super(message); this.name = 'HttpError'; this.status = status; this.data = data; }}
export class TimeoutError extends Error { constructor(timeout: number) { super(`Request timed out after ${timeout}ms`); this.name = 'TimeoutError'; }}
// http-client.ts (addition)async requestWithTimeout<T>( endpoint: string, options: RequestInit, timeout: number = 5000): Promise<ApiResponse<T>> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout);
try { const response = await fetch(`${this.baseUrl}${endpoint}`, { ...options, signal: controller.signal, });
if (!response.ok) { const errorData = await response.json().catch(() => null); throw new HttpError( `HTTP ${response.status}: ${response.statusText}`, response.status, errorData ); }
const data: T = await response.json(); r