HTTP client in TypeScript: making requests correctly
In modern web development, working with HTTP requests is one of the key tasks. TypeScript, as a strictly typed superset of JavaScript, provides developers with powerful tools for creating reliable and predictable HTTP clients. In this article, we will break down how to properly organize an HTTP client in TypeScript so that your code is safe, readable, and easily maintainable.
Why is TypeScript ideal for HTTP clients?
TypeScript adds static typing, which is especially important when working with external APIs. You know exactly what data will come from the server and can prevent many errors at the compilation stage. Let's look at a basic example:
// Bad: JavaScript without typesconst data = await fetch('/api/users');const users = await data.json();// We don't know the structure of users
// Good: TypeScript with typesinterface User { id: number; name: string; email: string;}
const response = await fetch('/api/users');const users: User[] = await response.json();// Now users has a clear structureCreating a basic HTTP client in TypeScript
Let's start by creating a universal HTTP client that will handle all types of requests. We'll use the native fetch API, which is available in modern browsers and Node.js 18+.
Basic implementation with typing
interface HttpClientConfig { baseURL: string; headers?: Record<string, string>; timeout?: number;}
interface ApiResponse<T> { data: T; status: number; ok: boolean;}
class HttpClient { private baseURL: string; private headers: Record<string, string>; private timeout: number;
constructor(config: HttpClientConfig) { this.baseURL = config.baseURL; this.headers = { 'Content-Type': 'application/json', ...config.headers, }; this.timeout = config.timeout || 10000; }
private async request<T>( endpoint: string, options: RequestInit = {} ): Promise<ApiResponse<T>> { const url = `${this.baseURL}${endpoint}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try { const response = await fetch(url, { ...options, headers: { ...this.headers, ...options.headers }, signal: controller.signal, });
clearTimeout(timeoutId);
const data: T = await response.json();
return { data, status: response.status, ok: response.ok, }; } catch (error) { clearTimeout(timeoutId); throw this.handleError(error); } }
private handleError(error: unknown): Error { if (error instanceof DOMException && error.name === 'AbortError') { return new Error('Request timeout'); } if (error instanceof TypeError) { return new Error('Network error'); } return error as Error; }
async get<T>(endpoint: string): Promise<ApiResponse<T>> { return this.request<T>(endpoint, { method: 'GET' }); }
async post<T>(endpoint: string, body: unknown): Promise<ApiResponse<T>> { return this.request<T>(endpoint, { method: 'POST', body: JSON.stringify(body), }); }
async put<T>(endpoint: string, body: unknown): Promise<ApiResponse<T>> { return this.request<T>(endpoint, { method: 'PUT', body: JSON.stringify(body), }); }
async delete<T>(endpoint: string): Promise<ApiResponse<T>> { return this.request<T>(endpoint, { method: 'DELETE' }); }}Using the client
// Create a client instanceconst api = new HttpClient({ baseURL: 'https://api.