NJ Municipality Lookup
CodebaseSrcAdapters

Http

HTTP client with timeout handling, exponential backoff retry logic, and errortransformation.

HTTP Client Adapter (adapters/http/)

HTTP client with timeout handling, exponential backoff retry logic, and errortransformation.

Implementation

fetch-client.ts

Implements HttpClientPort using native Fetch API with enhancements.

Features:

  • Timeout Control: Configurable per-request timeouts with AbortController
  • Exponential Backoff: Automatic retries with increasing delays
  • Error Transformation: Converts fetch errors to domain errors
  • JSON Handling: Automatic content-type detection and parsing
  • Logging: Detailed request/retry logging for debugging

API

interface HttpClientPort {
  request<T>(
    url: string,
    options: HttpRequestOptions,
  ): Promise<HttpResponse<T>>;
  get<T>(url: string, options?: HttpRequestOptions): Promise<HttpResponse<T>>;
  post<T>(
    url: string,
    body: unknown,
    options?: HttpRequestOptions,
  ): Promise<HttpResponse<T>>;
}

Request Options

interface HttpRequestOptions {
  method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
  headers?: Record<string, string>;
  body?: unknown;
  timeoutMs?: number; // Default: 10000ms
  maxRetries?: number; // Default: 3
  retryDelayMs?: number; // Default: 1000ms
  retryBackoffMultiplier?: number; // Default: 2
}

Response Structure

interface HttpResponse<T> {
  status: number;
  data: T;
  headers: Record<string, string>;
  fromCache: boolean;
}

Retry Logic

Exponential Backoff

Attempt 1: Immediate
Attempt 2: Wait 1000ms
Attempt 3: Wait 2000ms (1000 × 2)
Attempt 4: Wait 4000ms (2000 × 2)

Retry Conditions

Retries:

  • Network errors
  • Timeout errors (AbortError)

No Retry:

  • Invalid JSON responses (data validation errors)
  • Malformed API responses

Error Handling

Timeout

// After timeoutMs, request is aborted
throw new ApiTimeoutError({
  url: "https://api.example.com/data",
  timeoutMs: 10000,
  originalError: "AbortError",
});

Invalid Response

// Non-JSON or malformed response
throw new InvalidApiResponseError("Response is not valid JSON", {
  url,
  contentType: "text/html",
  bodyPreview: "<!DOCTYPE html>...",
});

Network Error

// Unknown errors wrapped
throw new InvalidApiResponseError("HTTP request failed", {
  url,
  originalError: "Network request failed",
});

Configuration

Default configuration from lib/config.ts:

njApi: {
  timeoutMs: 10000,
  retryEnabled: true,
  maxRetries: 3,
  retryDelayMs: 1000,
  retryBackoffMultiplier: 2
}

Usage Example

import { createFetchClient } from "./adapters/http/fetch-client";

const client = createFetchClient(logger);

// GET with timeout
const response = await client.get<ApiResponse>(url, {
  timeoutMs: 5000,
  maxRetries: 2,
});

// POST with body
const result = await client.post<CreateResponse>(url, {
  name: "example",
  value: 42,
});

Testing

  • Unit tests: Mocked fetch with various scenarios (success, timeout, retry, errors)
  • Integration tests: Real HTTP calls to public APIs (httpbin.org)
  • Timeout tests: Verify AbortController behavior
  • Retry tests: Verify exponential backoff timing
  • Error tests: Verify domain error transformations

Performance

  • Concurrent Requests: No built-in concurrency limits (manage at caller level)
  • Memory: Minimal overhead (~1KB per request)
  • Latency: Native fetch performance + retry delays

On this page