HttpClient
Overview
HttpClient is a thin, interceptor-based wrapper around the browser fetch API. It handles JSON serialization, timeout via AbortController, a pluggable interceptor chain, and typed responses. QueryCache provides a time-based in-memory cache that lives alongside the client.
Key Concepts
- Request interceptors run before the request is sent and can mutate
RequestConfig(add headers, sign tokens, etc.). - Response interceptors run after a successful response is received and can normalize or transform
ApiResponse. HttpErroris thrown for non-2xx responses, carrying the status code and parsed response body.- Default timeout is 10 000 ms; override per-request via
timeoutinRequestConfig. - The client sets
Content-Type: application/jsonby default. - Endpoints resolve against the base URL (
new HttpClient('https://api.example/v1/')thenget('users')). An endpoint on another origin, such as//other.example/xorhttps://other.example/x, is refused, so request paths built from user input cannot redirect requests (and their headers) elsewhere. Without a base URL, endpoints resolve against the page address; outside a browser they must be absolute.
API Reference
RequestConfig
interface RequestConfig {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; // default 'GET'
headers?: Record<string, string>;
body?: any; // serialized to JSON automatically
timeout?: number; // ms, default 10000
}
ApiResponse<T>
interface ApiResponse<T = any> {
data: T;
status: number;
statusText: string;
headers: Record<string, string>;
}
HttpError
class HttpError extends Error {
status: number;
statusText: string;
data?: any; // parsed response body, if available
}
HttpClient
class HttpClient {
constructor(baseURL?: string)
// Configuration
setBaseURL(url: string): void
setDefaultHeader(key: string, value: string): void
// Interceptors
addRequestInterceptor(fn: (config: RequestConfig) => RequestConfig): void
addResponseInterceptor(fn: (response: ApiResponse) => ApiResponse): void
// Methods
request<T>(endpoint: string, config?: RequestConfig): Promise<ApiResponse<T>>
get<T>(endpoint: string, config?: Omit<RequestConfig, 'method' | 'body'>): Promise<ApiResponse<T>>
post<T>(endpoint: string, data?: any, config?: Omit<RequestConfig, 'method' | 'body'>): Promise<ApiResponse<T>>
put<T>(endpoint: string, data?: any, config?: Omit<RequestConfig, 'method' | 'body'>): Promise<ApiResponse<T>>
delete<T>(endpoint: string, config?: Omit<RequestConfig, 'method' | 'body'>): Promise<ApiResponse<T>>
}
QueryCache
class QueryCache {
set(key: string, data: any, ttl?: number): void // ttl ms, default 300000 (5 min)
get(key: string): any | null // null if missing or expired
clear(): void
}
const queryCache: QueryCache // singleton instance
Basic Usage
import { HttpClient, HttpError } from '@tagna/udiot';
import { Injectable } from '@tagna/udiot';
@Injectable()
class UserService {
private http: HttpClient;
constructor() {
this.http = new HttpClient('https://api.example.com');
// Add auth header to every request
this.http.addRequestInterceptor((config) => ({
...config,
headers: {
...config.headers,
Authorization: `Bearer ${localStorage.getItem('token') ?? ''}`,
},
}));
}
async getUser(id: string) {
try {
const { data } = await this.http.get<User>(`/users/${id}`);
return data;
} catch (err) {
if (err instanceof HttpError && err.status === 404) {
return null;
}
throw err;
}
}
async createUser(dto: CreateUserDto) {
const { data } = await this.http.post<User>('/users', dto);
return data;
}
}
Common Patterns
Retry interceptor for transient failures
import { HttpClient, HttpError, ApiResponse } from '@tagna/udiot';
function withRetry(client: HttpClient, maxAttempts = 3): void {
// Response interceptors run after a successful fetch; for retries,
// wrap the request method instead:
const originalRequest = client.request.bind(client);
(client as any).request = async function <T>(
endpoint: string,
config = {}
): Promise<ApiResponse<T>> {
let lastError: unknown;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await originalRequest<T>(endpoint, config);
} catch (err) {
lastError = err;
if (err instanceof HttpError && (err.status === 429 || err.status === 503)) {
const delay = 1000 * 2 ** attempt;
await new Promise((r) => setTimeout(r, delay));
continue;
}
throw err;
}
}
throw lastError;
};
}
const http = new HttpClient('https://api.example.com');
withRetry(http);
Caching GET responses
import { HttpClient, queryCache } from '@tagna/udiot';
const http = new HttpClient('https://api.example.com');
async function getCachedUser(id: string): Promise<User> {
const cacheKey = `user:${id}`;
const cached = queryCache.get(cacheKey);
if (cached) return cached as User;
const { data } = await http.get<User>(`/users/${id}`);
queryCache.set(cacheKey, data, 60_000); // cache 1 minute
return data;
}
Integration Notes
HttpClientdoes not depend on any other framework module; it can be used standalone or inside@Injectable()services.- Request interceptors are applied in insertion order; response interceptors are also applied in insertion order (first added, first run).
HttpErroris separate from the telemetry error pipeline. Use the public telemetry tracing/logger functions to record HTTP errors; the internalAILoggeddecorator is not exported.- For SSR environments where
fetchmay not be global, polyfill it before constructing anHttpClient.
Pitfalls
- The
bodyfield is always serialized withJSON.stringify. Binary or form-data bodies require a custom request usingfetchdirectly or a subclass. - Response interceptors run only on successful responses (2xx). Errors thrown before or after the fetch call bypass the response interceptor chain.
QueryCacheis an in-memory singleton; it does not persist across page reloads. For durable caching combine it with@Persiston a store.- Setting
timeoutto0does not disable the timeout — the default of 10 000 ms still applies. To effectively disable, pass a very large number.
From the canonical UDIOT public documentation.Try it in the playground ↗