Docs/enterprisev1.0.0-rc.1

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.
  • HttpError is thrown for non-2xx responses, carrying the status code and parsed response body.
  • Default timeout is 10 000 ms; override per-request via timeout in RequestConfig.
  • The client sets Content-Type: application/json by default.
  • Endpoints resolve against the base URL (new HttpClient('https://api.example/v1/') then get('users')). An endpoint on another origin, such as //other.example/x or https://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

  • HttpClient does 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).
  • HttpError is separate from the telemetry error pipeline. Use the public telemetry tracing/logger functions to record HTTP errors; the internal AILogged decorator is not exported.
  • For SSR environments where fetch may not be global, polyfill it before constructing an HttpClient.

Pitfalls

  • The body field is always serialized with JSON.stringify. Binary or form-data bodies require a custom request using fetch directly or a subclass.
  • Response interceptors run only on successful responses (2xx). Errors thrown before or after the fetch call bypass the response interceptor chain.
  • QueryCache is an in-memory singleton; it does not persist across page reloads. For durable caching combine it with @Persist on a store.
  • Setting timeout to 0 does 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 ↗