docs-site/typedoc-src/services/HttpClient.tstypescript
/**
* HTTP Client Service
*
* @remarks
* Provides a typed HTTP client for making API requests with
* automatic retry, timeout, and error handling.
*
* @packageDocumentation
*/
/**
* HTTP methods supported by the client
*
* @public
*/
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
/**
* Request configuration options
*
* @public
*/
export interface RequestConfig {
/**
* Request headers
*/
headers?: Record<string, string>
/**
* Query parameters
*/
params?: Record<string, string | number | boolean>
/**
* Request timeout in milliseconds
* @defaultValue 30000
*/
timeout?: number
/**
* Number of retry attempts on failure
* @defaultValue 0
*/
retries?: number
/**
* Delay between retries in milliseconds
* @defaultValue 1000
*/
retryDelay?: number
/**
* Whether to include credentials
* @defaultValue false
*/
withCredentials?: boolean
/**
* Response type expectation
* @defaultValue 'json'
*/
responseType?: 'json' | 'text' | 'blob' | 'arraybuffer'
/**
* Abort signal for cancellation
*/
signal?: AbortSignal
}
/**
* HTTP response wrapper
*
* @typeParam T - Response data type
* @public
*/
export interface HttpResponse<T = unknown> {
/** Response data */
data: T
/** HTTP status code */
status: number
/** Status text */
statusText: string
/** Response headers */
headers: Record<string, string>
/** Original request config */
config: RequestConfig
/** Request duration in milliseconds */
duration: number
}
/**
* HTTP error class
*
* @remarks
* Thrown when an HTTP request fails. Contains detailed
* information about the failure.
*
* @example
* ```typescript
* try {
* await httpClient.get('/api/users')
* } catch (error) {
* if (error instanceof HttpError) {
* console.log(`Request failed: ${error.status} - ${error.message}`)
* }
* }
* ```
*
* @public
*/
export class HttpError extends Error {
/**
* HTTP status code
*/
public readonly status: number
/**
* Response data if available
*/
public readonly data?: unknown
/**
* Request configuration that caused the error
*/
public readonly config: RequestConfig
/**
* Request URL
*/
public readonly url: string
/**
* Creates a new HttpError
*
* @param message - Error message
* @param status - HTTP status code
* @param url - Request URL
* @param config - Request configuration
* @param data - Response data if available
*/
constructor(message: string, status: number, url: string, config: RequestConfig, data?: unknown) {
super(message)
this.name = 'HttpError'
this.status = status
this.url = url
this.config = config
this.data = data
}
/**
* Checks if error is a client error (4xx)
*/
public isClientError(): boolean {
return this.status >= 400 && this.status < 500
}
/**
* Checks if error is a server error (5xx)
*/
public isServerError(): boolean {
return this.status >= 500 && this.status < 600
}
}
/**
* Request interceptor function type
*
* @param config - Request configuration
* @returns Modified configuration or promise
* @public
*/
export type RequestInterceptor = (config: RequestConfig) => RequestConfig | Promise<RequestConfig>
/**
* Response interceptor function type
*
* @typeParam T - Response data type
* @param response - HTTP response
* @returns Modified response or promise
* @public
*/
export type ResponseInterceptor<T = unknown> = (
response: HttpResponse<T>
) => HttpResponse<T> | Promise<HttpResponse<T>>
/**
* HTTP Client class for making API requests
*
* @remarks
* A comprehensive HTTP client with support for:
* - All standard HTTP methods
* - Request/response interceptors
* - Automatic retries
* - Timeout handling
* - TypeScript generics for type-safe responses
*
* @example
* Creating and configuring a client:
* ```typescript
* const client = new HttpClient({
* baseUrl: 'https://api.example.com',
* defaultTimeout: 5000,
* defaultHeaders: {
* 'Content-Type': 'application/json',
* 'Authorization': 'Bearer token'
* }
* })
*
* // Add request interceptor
* client.addRequestInterceptor((config) => {
* config.headers = {
* ...config.headers,
* 'X-Request-ID': generateRequestId()
* }
* return config
* })
* ```
*
* @example
* Making requests:
* ```typescript
* // GET request with type safety
* const users = await client.get<User[]>('/users')
*
* // POST request with body
* const newUser = await client.post<User>('/users', {
* name: 'John',
* email: 'john@example.com'
* })
*
* // Request with custom config
* const data = await client.get<Data>('/data', {
* timeout: 60000,
* retries: 3
* })
* ```
*
* @public
*/
export class HttpClient {
/**
* Base URL for all requests
* @readonly
*/
public readonly baseUrl: string
/**
* Default request timeout
*/
private defaultTimeout: number
/**
* Default headers applied to all requests
*/
private defaultHeaders: Record<string, string>
/**
* Request interceptors
*/
private requestInterceptors: RequestInterceptor[] = []
/**
* Response interceptors
*/
private responseInterceptors: ResponseInterceptor[] = []
/**
* Creates a new HttpClient instance
*
* @param options - Client configuration options
* @param options.baseUrl - Base URL for all requests
* @param options.defaultTimeout - Default timeout in milliseconds
* @param options.defaultHeaders - Default headers for all requests
*/
constructor(options: {
baseUrl: string
defaultTimeout?: number
defaultHeaders?: Record<string, string>
}) {
this.baseUrl = options.baseUrl.replace(/\/$/, '')
this.defaultTimeout = options.defaultTimeout ?? 30000
this.defaultHeaders = options.defaultHeaders ?? {}
}
/**
* Adds a request interceptor
*
* @param interceptor - Interceptor function
* @returns Function to remove the interceptor
*
* @example
* ```typescript
* const remove = client.addRequestInterceptor((config) => {
* console.log('Request:', config)
* return config
* })
*
* // Later: remove the interceptor
* remove()
* ```
*/
public addRequestInterceptor(interceptor: RequestInterceptor): () => void {
this.requestInterceptors.push(interceptor)
return () => {
const index = this.requestInterceptors.indexOf(interceptor)
if (index !== -1) {
this.requestInterceptors.splice(index, 1)
}
}
}
/**
* Adds a response interceptor
*
* @param interceptor - Interceptor function
* @returns Function to remove the interceptor
*/
public addResponseInterceptor<T = unknown>(interceptor: ResponseInterceptor<T>): () => void {
this.responseInterceptors.push(interceptor as ResponseInterceptor)
return () => {
const index = this.responseInterceptors.indexOf(interceptor as ResponseInterceptor)
if (index !== -1) {
this.responseInterceptors.splice(index, 1)
}
}
}
/**
* Performs a GET request
*
* @typeParam T - Expected response data type
* @param path - Request path
* @param config - Request configuration
* @returns Promise resolving to the response
*
* @example
* ```typescript
* const user = await client.get<User>('/users/123')
* console.log(user.data.name)
* ```
*/
public async get<T = unknown>(path: string, config?: RequestConfig): Promise<HttpResponse<T>> {
return this.request<T>('GET', path, undefined, config)
}
/**
* Performs a POST request
*
* @typeParam T - Expected response data type
* @typeParam D - Request body data type
* @param path - Request path
* @param data - Request body data
* @param config - Request configuration
* @returns Promise resolving to the response
*/
public async post<T = unknown, D = unknown>(
path: string,
data?: D,
config?: RequestConfig
): Promise<HttpResponse<T>> {
return this.request<T>('POST', path, data, config)
}
/**
* Performs a PUT request
*
* @typeParam T - Expected response data type
* @typeParam D - Request body data type
* @param path - Request path
* @param data - Request body data
* @param config - Request configuration
* @returns Promise resolving to the response
*/
public async put<T = unknown, D = unknown>(
path: string,
data?: D,
config?: RequestConfig
): Promise<HttpResponse<T>> {
return this.request<T>('PUT', path, data, config)
}
/**
* Performs a PATCH request
*
* @typeParam T - Expected response data type
* @typeParam D - Request body data type
* @param path - Request path
* @param data - Request body data
* @param config - Request configuration
* @returns Promise resolving to the response
*/
public async patch<T = unknown, D = unknown>(
path: string,
data?: D,
config?: RequestConfig
): Promise<HttpResponse<T>> {
return this.request<T>('PATCH', path, data, config)
}
/**
* Performs a DELETE request
*
* @typeParam T - Expected response data type
* @param path - Request path
* @param config - Request configuration
* @returns Promise resolving to the response
*/
public async delete<T = unknown>(path: string, config?: RequestConfig): Promise<HttpResponse<T>> {
return this.request<T>('DELETE', path, undefined, config)
}
/**
* Performs an HTTP request
*
* @typeParam T - Expected response data type
* @param method - HTTP method
* @param path - Request path
* @param data - Request body data
* @param config - Request configuration
* @returns Promise resolving to the response
* @throws {@link HttpError} On request failure
*
* @internal
*/
private async request<T>(
method: HttpMethod,
path: string,
data?: unknown,
config: RequestConfig = {}
): Promise<HttpResponse<T>> {
const startTime = Date.now()
// Apply request interceptors
let finalConfig: RequestConfig = {
...config,
headers: { ...this.defaultHeaders, ...config.headers },
timeout: config.timeout ?? this.defaultTimeout,
}
for (const interceptor of this.requestInterceptors) {
finalConfig = await interceptor(finalConfig)
}
// Build URL with query params
const url = this.buildUrl(path, finalConfig.params)
// Simulate HTTP request (in real implementation, use fetch/axios)
const response: HttpResponse<T> = {
data: {} as T,
status: 200,
statusText: 'OK',
headers: {},
config: finalConfig,
duration: Date.now() - startTime,
}
// Apply response interceptors
let finalResponse = response
for (const interceptor of this.responseInterceptors) {
finalResponse = (await interceptor(finalResponse)) as HttpResponse<T>
}
return finalResponse
}
/**
* Builds a full URL with query parameters
*
* @param path - Request path
* @param params - Query parameters
* @returns Full URL string
*
* @internal
*/
private buildUrl(path: string, params?: Record<string, string | number | boolean>): string {
const url = new URL(path.startsWith('/') ? path.slice(1) : path, this.baseUrl + '/')
if (params) {
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, String(value))
}
}
return url.toString()
}
}
/**
* Creates a pre-configured HTTP client
*
* @param baseUrl - Base URL for the client
* @returns Configured HttpClient instance
*
* @example
* ```typescript
* const api = createHttpClient('https://api.example.com')
* const users = await api.get<User[]>('/users')
* ```
*
* @public
*/
export function createHttpClient(baseUrl: string): HttpClient {
return new HttpClient({
baseUrl,
defaultTimeout: 30000,
defaultHeaders: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
})
}