Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FetchTheWrap

A production-grade HTTP request library for TypeScript. Fast, tiny, fully typed — Fetch evolved.

npm install fetch-the-wrap

Table of Contents

  1. Why FetchTheWrap?
  2. Quick Start
  3. API Reference
  4. Error Classes
  5. Constants
  6. Header Utilities
  7. URL Utilities
  8. Body Utilities
  9. Merge Utilities
  10. Adapter Functions
  11. Middleware System
  12. Middleware Factories
  13. Cookie Functions
  14. Parser Functions
  15. Plugin Functions
  16. Type Exports

Why FetchTheWrap?

FetchTheWrap is a modern HTTP client that stays in the same performance class as native fetch while adding capabilities that fetch does not provide:

  • Full TypeScript generics — typed requests, responses, and errors
  • Middleware / interceptor pipeline (retry, cache, timeout, circuit breaker, throttling, tracing, cookies, progress, dedup)
  • Cookie jar with Set-Cookie parsing and persistence
  • Retry with exponential backoff, jitter, and configurable predicates
  • Cache (in-memory) with TTL and ETag awareness
  • Request deduplication (in-flight de-duplication)
  • Circuit breaker pattern
  • Rate limiting / throttling
  • Upload/download progress events
  • Distributed tracing (W3C, B3, Jaeger, Datadog)
  • GraphQL helper
  • SSE / EventSource support
  • Mock adapter for testing
  • Environment-aware (browser + Node.js)
  • Tree-shakeable ESM + CJS

Quick Start

import { fetchTheWrap } from 'fetch-the-wrap';

// Simple GET
const res = await fetchTheWrap.get('https://api.example.com/users');
console.log(res.data);

// POST with typed response
interface User { id: number; name: string }
const res = await fetchTheWrap.post<User>('https://api.example.com/users', { name: 'Alice' });
console.log(res.data.id);

// With query params and headers
const res = await fetchTheWrap.get('https://api.example.com/search', {
  params: { q: 'typescript', page: 1 },
  headers: { 'x-api-key': 'secret' },
});

// Reusable client
import { createClient } from 'fetch-the-wrap';

const api = createClient({
  baseURL: 'https://api.example.com',
  headers: { 'x-api-key': 'my-key' },
  timeout: 10_000,
  retry: { limit: 3, backoff: 'exponential' },
});

const users = await api.get('/users');
const created = await api.post('/users', { name: 'Bob' });

API Reference

createClient(defaults?: Partial<ClientOptions>): ClientInstance

Source: src/core/client.ts:34

Creates a composable ClientInstance with the given defaults.

import { createClient } from 'fetch-the-wrap';

const client = createClient({
  baseURL: 'https://api.example.com',
  headers: { 'x-api-key': 'key' },
  timeout: 10_000,
  retry: { limit: 3 },
});

ClientInstance Methods

Returned by createClient(). Also available on the fetchTheWrap singleton.

Method Signature Source Description
callable (url, options?) => Promise<FetchTheWrapResponse<T>> client.ts:224 Direct invocation like client(url, opts)
get <T>(url, options?) => Promise<FetchTheWrapResponse<T>> client.ts:227 HTTP GET
post <T>(url, body?, options?) => Promise<FetchTheWrapResponse<T>> client.ts:230 HTTP POST with body
put <T>(url, body?, options?) => Promise<FetchTheWrapResponse<T>> client.ts:233 HTTP PUT with body
patch <T>(url, body?, options?) => Promise<FetchTheWrapResponse<T>> client.ts:236 HTTP PATCH with body
delete <T>(url, options?) => Promise<FetchTheWrapResponse<T>> client.ts:239 HTTP DELETE
head <T>(url, options?) => Promise<FetchTheWrapResponse<T>> client.ts:242 HTTP HEAD
options <T>(url, options?) => Promise<FetchTheWrapResponse<T>> client.ts:245 HTTP OPTIONS
extend (defaults?) => ClientInstance client.ts:251 New client with merged defaults
create (defaults?) => ClientInstance client.ts:248 Alias for extend
use (middleware) => ClientInstance client.ts:254 Add middleware to pipeline
hooks Hooks (property) client.ts:259 Lifecycle hooks accessor
defaults Partial<ClientOptions> (property) client.ts:268 Current config
// Extend creates a new instance
const authed = api.extend({
  headers: { authorization: 'Bearer token' },
});

// use adds middleware (builder pattern)
api.use(retryMiddleware({ limit: 3 }));
api.use(timeoutMiddleware(5000));

// hooks as properties
api.hooks.beforeRequest.push((cfg) => {
  cfg.headers['x-trace'] = crypto.randomUUID();
});

FetchTheWrapResponse<T> Methods

Every request returns FetchTheWrapResponse<T>, which extends InternalResponse with parsing helpers.

Method / Property Signature Source Description
data T client.ts:201 Auto-parsed response body
json <U = T>() => Promise<U> client.ts:167 Parse as JSON
text () => Promise<string> client.ts:174 Parse as text
blob () => Promise<Blob> client.ts:180 Parse as Blob
arrayBuffer () => Promise<ArrayBuffer> client.ts:186 Parse as ArrayBuffer
formData () => Promise<FormData> client.ts:192 Parse as FormData
status number HTTP status code
statusText string HTTP status text
headers Record<string, string> Normalized response headers
url string Final response URL
ok boolean Status in 200-299 range
redirected boolean Whether request was redirected
roundTripTime number Total time in ms
redirectHistory RedirectEntry[] Redirect chain
timings ResponseTimings Phase timings
retryAttempt number Which retry attempt (0=first)
fromCache boolean Whether served from cache
raw Response Original native Response
const res = await fetchTheWrap.get('/user/1');
console.log(res.data);              // auto-parsed from content-type
console.log(res.status);            // 200
console.log(res.roundTripTime);     // 143ms
console.log(res.headers['x-request-id']);

// Re-parse if needed
const text = await res.text();

Error Classes

Source: src/core/errors.ts

Class Constructor Extends Code Retryable When
FetchTheWrapError (message, opts?) Error false Base error class
HTTPError (message, response, request?) FetchTheWrapError HTTP_{status} 5xx/429 Non-2xx response
TimeoutError (message, opts?) FetchTheWrapError TIMEOUT true Request exceeded timeout
AbortError (message, opts?) FetchTheWrapError ABORTED false Request was aborted
ConnectionError (message, opts?) FetchTheWrapError CONNECTION_ERROR true Network failure
ParseError (message, opts?) FetchTheWrapError PARSE_ERROR false Body parse failure
RedirectError (message, opts?) FetchTheWrapError REDIRECT_ERROR false Redirect policy violation
CircuitBreakerError (message, opts?) FetchTheWrapError CIRCUIT_OPEN false Circuit breaker open
RateLimitError (message, opts?) FetchTheWrapError RATE_LIMITED true Throttle limit hit

All error constructors accept FetchTheWrapErrorOptions:

{
  status?, statusText?, url?, method?, headers?, body?,
  code?, retryAttempt?, retryable?, timings?, response?, request?, cause?
}

HTTPError is constructed from an InternalResponse and auto-derives code from response.status and retryable from 5xx/429.

try {
  await fetchTheWrap.get('https://api.example.com/nonexistent');
} catch (err) {
  if (err instanceof HTTPError) {
    console.error(`HTTP ${err.status}: ${err.message}`);
    console.error('Retryable:', err.retryable);
  } else if (err instanceof TimeoutError) {
    console.error('Request timed out');
  } else if (err instanceof FetchTheWrapError) {
    console.error(err.code, err.url, err.method);
  }
}

Constants

Source: src/core/constants.ts

HTTP Methods

Constant Value
METHODS ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS','CONNECT','TRACE']
METHODS_WITH_BODY ['POST','PUT','PATCH','DELETE']
RETRYABLE_METHODS ['GET','HEAD','OPTIONS','TRACE']
RETRYABLE_STATUSES [408, 429, 500, 502, 503, 504]

Default Config

Constant Value
DEFAULT_TIMEOUT { request: 30000, connection: 10000, idle: 5000 }
DEFAULT_RETRY { limit: 0, delay: 1000, maxDelay: 30000, statusCodes: [...], networkErrors: true, backoff: 'exponential', jitter: true, retryAfter: true }
DEFAULTS { timeout, redirect: 'follow', maxRedirects: 20, compress: true, keepalive: true, retry }

Content-Type Constants

Constant Value
CONTENT_TYPE_JSON 'application/json'
CONTENT_TYPE_FORM 'application/x-www-form-urlencoded'
CONTENT_TYPE_MULTIPART 'multipart/form-data'
CONTENT_TYPE_TEXT 'text/plain'
CONTENT_TYPE_OCTET 'application/octet-stream'

Header Name Constants

Constant Value
HEADER_CONTENT_TYPE 'content-type'
HEADER_ACCEPT 'accept'
HEADER_AUTHORIZATION 'authorization'
HEADER_USER_AGENT 'user-agent'
HEADER_SET_COOKIE 'set-cookie'
HEADER_COOKIE 'cookie'
HEADER_RETRY_AFTER 'retry-after'
HEADER_CONTENT_LENGTH 'content-length'
HEADER_TRACE_ID 'x-trace-id'
HEADER_SPAN_ID 'x-span-id'
HEADER_IDEMPOTENCY_KEY 'idempotency-key'
HEADER_REQUEST_ID 'x-request-id'

Other

Constant Value
USER_AGENT 'fetch-the-wrap/0.1.0'

Header Utilities

Source: src/utils/headers.ts

normalizeHeaderName(name)

function normalizeHeaderName(name: string): string

Lowercases and trims a header name. Uses an LRU cache (max 1000 entries) to avoid repeated string operations on hot paths.

normalizeHeaders(input)

function normalizeHeaders(input: HeadersInit | undefined): Record<string, string>

Converts any HeadersInit (Headers object, [key, value] tuple array, or plain record) to a normalized Record<string, string>. Skips undefined/null values.

normalizeHeaders({ 'Content-Type': 'application/json' });
//   { 'content-type': 'application/json' }

normalizeHeaders(new Headers({ 'X-Custom': 'val' }));
//   { 'x-custom': 'val' }

mergeHeaders(...sources)

function mergeHeaders(...sources: (HeadersInit | undefined)[]): Record<string, string>

Merges multiple header sources left-to-right. Later sources override earlier ones.

getHeader(headers, name)

function getHeader(headers: Record<string, string>, name: string): string | undefined

Case-insensitive header read.

setHeader(headers, name, value)

function setHeader(headers: Record<string, string>, name: string, value: HeaderValue): void

Case-insensitive header write. Array values are joined with ', '. undefined/null values remove the header.

hasHeader(headers, name)

function hasHeader(headers: Record<string, string>, name: string): boolean

Case-insensitive header existence check.

removeHeader(headers, name)

function removeHeader(headers: Record<string, string>, name: string): void

Case-insensitive header removal.

parseContentType(contentType)

function parseContentType(contentType: string): { type: string; subtype: string; params: Record<string, string> }

Parses "application/json; charset=utf-8" into { type: "application", subtype: "json", params: { charset: "utf-8" } }.

inferContentType(body)

function inferContentType(body: unknown): string | undefined

Auto-detects content-type from body type:

  • FormData multipart/form-data
  • URLSearchParams application/x-www-form-urlencoded
  • Blob blob's own type
  • string text/plain
  • ArrayBuffer / ArrayBufferView application/octet-stream
  • object application/json

isJSONContentType(contentType)

function isJSONContentType(contentType: string): boolean

Checks if a content-type string is application/json or application/*+json.


URL Utilities

Source: src/utils/url.ts

serializeQueryParams(params)

function serializeQueryParams(params: QueryParams | undefined): string

Serializes query parameters to a string. Accepts string, URLSearchParams, string[][], or Record<string, QueryParamValue>. Handles array values (repeated key), filters null/undefined, encodes special characters.

serializeQueryParams({ a: '1', b: ['x', 'y'] });
//   'a=1&b=x&b=y'

serializeQueryParams({ name: 'hello world' });
//   'name=hello%20world'

resolveURL(baseURL, url, params?)

function resolveURL(baseURL: string | undefined, url: string, params?: QueryParams): string

Resolves a URL against a base URL. If url is absolute, base is ignored. Appends query params if provided. Ensures single / between base and path.

resolveURL('https://api.example.com', '/v1/users', { page: '1' });
//   'https://api.example.com/v1/users?page=1'

isAbsoluteURL(url)

function isAbsoluteURL(url: string): boolean

Returns true if URL has a protocol scheme or starts with //.

parseQueryString(query)

function parseQueryString(query: string): Record<string, string>

Decodes a query string into a record. Handles leading ?, + for spaces.

buildURL(parts)

function buildURL(parts: { protocol?, host?, port?, path?, params? }): string

Constructs a full URL from components. Adds :// after protocol, : before port, / before path if missing, appends query string.


Body Utilities

Source: src/utils/body.ts

serializeBody(body, headers)

function serializeBody(body: unknown, headers: Record<string, string>): RequestBody

Serializes a request body to a fetch-compatible type. Passes through FormData, URLSearchParams, Blob, ArrayBuffer, ArrayBufferView, string, ReadableStream as-is. For objects: if content-type is form, serializes as URL-encoded; otherwise serializes as JSON. Automatically sets content-type header if missing.

isBodySerializable(value)

function isBodySerializable(value: unknown): value is RequestBodySerializable

Type guard checking if a value can be serialized (string, number, boolean, null, plain object, array of serializables).

parseBody<T>(response, type?)

async function parseBody<T = unknown>(response: Response, type?: string): Promise<T>

Parses a response body. With explicit type ('json'/'text'/'blob'/'arrayBuffer'/'formData'/'stream') uses that parser. Without type, auto-detects from content-type header: json for application/json/+json, text for text/*, formData for multipart/form-data.

streamBody(response)

async function* streamBody(response: Response): AsyncGenerator<Uint8Array, void, unknown>

Async generator yielding Uint8Array chunks from a response body stream. Releases reader lock in finally.

for await (const chunk of streamBody(response)) {
  console.log(chunk.byteLength, 'bytes received');
}

createFormData(data, form?)

function createFormData(data: Record<string, unknown>, form?: FormData): FormData

Creates or appends to a FormData from a record. Handles Blob values (file upload), arrays (repeated keys), and primitives (toString).


Merge Utilities

Source: src/utils/merge.ts

deepMerge<T>(target, ...sources)

function deepMerge<T = Record<string, unknown>>(target, ...sources): T

Recursively merges plain objects. Arrays are replaced, not merged. Skips undefined values in sources. Returns typed result.

const a = { x: 1, y: { z: 2 } };
const b = { y: { w: 3 } };
deepMerge(a, b);
//   { x: 1, y: { z: 2, w: 3 } }

pick<T, K>(obj, keys)

function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>

Returns a new object with only the specified keys.

omit<T, K>(obj, keys)

function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K>

Returns a new object without the specified keys.

cloneDeep<T>(value)

function cloneDeep<T>(value: T): T

Deep clones a value. Handles Date, RegExp, Map, Set, Blob, ArrayBuffer, URLSearchParams, FormData by reference (they're immutable or have their own clone).


Adapter Functions

Source: src/adapters/adapter.ts

detectAdapter()

function detectAdapter(): RequestAdapter

Auto-detects the runtime environment. Returns browser adapter if globalThis.fetch exists, otherwise Node adapter.

getDefaultAdapter()

function getDefaultAdapter(): RequestAdapter

Returns the cached default adapter. Lazily initializes via detectAdapter() on first call.

setDefaultAdapter(adapter)

function setDefaultAdapter(adapter: RequestAdapter): void

Overrides the default adapter. Used by the mock plugin and for custom adapters.

RequestAdapter (type)

type RequestAdapter = (config: InternalRequestConfig) => Promise<InternalResponse>

The adapter is a function that receives a normalized InternalRequestConfig and returns an InternalResponse. The default browser adapter passes options directly to fetch(). To create a custom adapter:

import { setDefaultAdapter } from 'fetch-the-wrap';

setDefaultAdapter(async (config) => {
  const response = await fetch(config.url, {
    method: config.method,
    headers: config.headers,
    body: config.body as BodyInit,
    signal: config.signal,
  });

  const headers: Record<string, string> = {};
  response.headers.forEach((v, k) => { headers[k.toLowerCase()] = v; });

  return {
    status: response.status,
    statusText: response.statusText,
    headers,
    body: response.body,
    url: response.url,
    ok: response.ok,
    redirected: response.redirected,
    bodyType: config.responseType ?? null,
    bodySize: null,
    roundTripTime: 0,
    redirectHistory: [],
    timings: { start: 0, firstByte: 0, download: 0, total: 0 },
    retryAttempt: 0,
    fromCache: false,
    raw: response,
  };
});

Middleware System

Source: src/middleware/index.ts

MiddlewareFn (type)

type MiddlewareFn = (next: () => Promise<InternalResponse>, config: InternalRequestConfig) => Promise<InternalResponse>

Each middleware receives a next() function to pass control downstream and the config object. Middleware can:

  • Modify config before passing downstream
  • Intercept and short-circuit the response
  • Modify response after it returns
  • Catch errors from downstream
  • Execute code before and after next()
// Custom middleware: add timestamp header
const timestampMiddleware: MiddlewareFn = async (next, config) => {
  config.headers['x-request-start'] = Date.now().toString();
  const response = await next();
  response.headers['x-request-duration'] = String(Date.now() - Number(config.headers['x-request-start']));
  return response;
};

client.use(timestampMiddleware);

composeMiddleware(middlewares)

function composeMiddleware(middlewares: MiddlewareFn[]): (config, final) => Promise<InternalResponse>

Koa-style middleware composition. Returns a function taking (config, final) where final is the innermost handler. Each middleware receives (next, config) and must return Promise<InternalResponse>. Guards against multiple next() calls.

applyHooks(config)

function applyHooks(config: InternalRequestConfig): InternalRequestConfig

Converts lifecycle hooks (beforeRequest, afterResponse, onError) into MiddlewareFn entries and appends them to config.middleware. Called during request config construction.


Middleware Factories

retryMiddleware(options?)

Source: src/middleware/retry.ts:5

function retryMiddleware(options?: RetryOptions): MiddlewareFn
Option Type Default Description
limit number 2 Max retry attempts
delay number 1000 Base delay in ms
maxDelay number 30000 Max delay cap
statusCodes number[] [408, 429, 500, 502, 503, 504] Retryable response statuses
methods Method[] ['GET','HEAD','OPTIONS','TRACE'] Methods eligible for retry
networkErrors boolean true Retry on network failures
backoff 'linear' | 'exponential' | 'decorrelated' 'exponential' Backoff strategy
jitter boolean | number true Random variance (0.3 = 30%)
retryAfter boolean true Respect Retry-After header
predicate (attempt, error, response) => boolean | Promise<boolean> Custom retry decision

Backoff formulas (with delay = 1000, attempt = 2):

  • linear: 1000 * (2 + 1) = 3000ms
  • exponential: 1000 * 2^2 = 4000ms
  • decorrelated: random * (1000 * 2^(2+1)) = random up to 8000ms
import { retryMiddleware } from 'fetch-the-wrap';

client.use(retryMiddleware({
  limit: 3,
  backoff: 'exponential',
  jitter: 0.2,
  retryAfter: true,
  predicate: (attempt, error, response) => {
    // Don't retry on 402 Payment Required
    if (response?.status === 402) return false;
    return true;
  },
}));

timeoutMiddleware(timeout?)

Source: src/middleware/timeout.ts:9

function timeoutMiddleware(timeout?: number | TimeoutConfig): MiddlewareFn

Creates an AbortController with the configured timeout. Propagates external AbortSignal from config. Throws TimeoutError on expiry.

// As a number (request timeout in ms)
client.use(timeoutMiddleware(5000));

// As a config object
client.use(timeoutMiddleware({
  request: 10000,   // total request timeout
  connection: 5000, // connection phase timeout
  idle: 2000,       // idle phase timeout
}));

cacheMiddleware(options?)

Source: src/middleware/cache.ts:9

function cacheMiddleware(options?: CacheOptions): MiddlewareFn
Option Type Default Description
enabled boolean true Enable caching
type 'memory' | 'localStorage' | 'sessionStorage' | 'custom' 'memory' Store type
ttl number 300000 (5 min) Time-to-live in ms
maxEntries number 500 Max cache entries
methods Method[] ['GET'] Methods to cache
predicate (config) => boolean Request filter
adapter CacheAdapter Custom storage adapter

Custom CacheAdapter interface:

interface CacheAdapter {
  get(key: string): Promise<CacheEntry | undefined>;
  set(key: string, entry: CacheEntry, ttl: number): Promise<void>;
  delete(key: string): Promise<boolean>;
  clear(): Promise<void>;
  has(key: string): Promise<boolean>;
}
client.use(cacheMiddleware({
  ttl: 60_000,
  maxEntries: 200,
  methods: ['GET'],
  predicate: (config) => !config.url.includes('/auth'),
}));

dedupMiddleware(options?)

Source: src/middleware/dedup.ts:6

function dedupMiddleware(options?: DedupOptions): MiddlewareFn
Option Type Default Description
enabled boolean true Enable deduplication
key (config) => string {method}:{url} Custom dedup key
maxAge number 5000 How long to remember completed requests (ms)

Deduplicates in-flight requests — 10 concurrent GETs to /users/1 fire only one actual request.

client.use(dedupMiddleware({ enabled: true }));

clearDedupCache()

function clearDedupCache(): void

Clears all in-flight dedup promises. Source: dedup.ts:63.


circuitBreakerMiddleware(options?)

Source: src/middleware/circuit.ts:16

function circuitBreakerMiddleware(options?: CircuitBreakerOptions): MiddlewareFn
Option Type Default Description
enabled boolean true Enable circuit breaker
threshold number 5 Consecutive failures before opening
cooldown number 30000 ms before transitioning to half-open
halfOpenMaxRequests number 3 Probe requests allowed in half-open
monitoredStatuses number[] [500, 502, 503, 504] Response codes counted as failures

States: closed (normal) open (failing, rejects immediately) half-open (probing) back to closed or open.

resetAllCircuits()

function resetAllCircuits(): void

Clears all circuit breaker states. Source: circuit.ts:68.


throttleMiddleware(options?)

Source: src/middleware/throttle.ts:7

function throttleMiddleware(options?: ThrottleOptions): MiddlewareFn
Option Type Default Description
enabled boolean true Enable throttling
maxRequests number 10 Max requests per window
windowMs number 1000 Window duration in ms
queue boolean true Queue excess requests (false = throw RateLimitError)

resetThrottle()

function resetThrottle(): void

Clears all throttle states. Source: throttle.ts:67.


progressMiddleware(options?)

Source: src/middleware/progress.ts:7

function progressMiddleware(options?: ProgressOptions): MiddlewareFn
Option Type Default Description
enabled boolean true Enable progress events
onUpload (info: ProgressInfo) => void Upload progress callback
onDownload (info: ProgressInfo) => void Download progress callback
throttle number 100 Minimum interval between events (ms)

ProgressInfo shape:

interface ProgressInfo {
  direction: 'upload' | 'download';
  bytes: number;
  total: number | null;
  percent: number | null;
  phase: 'start' | 'progress' | 'complete';
}
client.use(progressMiddleware({
  onUpload: (info) => console.log(`Upload: ${info.percent}%`),
  onDownload: (info) => console.log(`Download: ${info.percent}%`),
  throttle: 250,
}));

cookieMiddleware(options?)

Source: src/middleware/cookies.ts:7

function cookieMiddleware(options?: CookieOptions): MiddlewareFn
Option Type Default Description
enabled boolean true Enable cookie handling
jar CookieJar Cookie jar instance from createCookieJar()

Requires a CookieJar instance. Automatically sends matching cookies on requests and stores Set-Cookie headers from responses.

const jar = createCookieJar();
client.use(cookieMiddleware({ jar }));

// Now cookies persist across requests
await client.get('https://example.com/login');   // stores session cookie
await client.get('https://example.com/dashboard'); // sends stored cookie

tracingMiddleware(options?)

Source: src/middleware/tracing.ts:7

function tracingMiddleware(options?: TracingOptions): MiddlewareFn
Option Type Default Description
enabled boolean true Enable tracing
traceId string auto-generated 32 hex char trace ID
spanId string auto-generated 16 hex char span ID
traceFlags string '01' W3C trace flags
propagate boolean true Inject tracing headers
headerFormat 'w3c' | 'b3' | 'jaeger' | 'datadog' | 'custom' 'w3c' Tracing format
customHeaders Record<string, string> Custom headers for 'custom' format

Header formats:

  • w3c: traceparent: 00-{traceId}-{spanId}-{traceFlags}
  • b3: x-b3-traceid, x-b3-spanid
  • jaeger: uber-trace-id: {traceId}:{spanId}
  • datadog: x-datadog-trace-id, x-datadog-parent-id

Cookie Functions

Source: src/cookies/jar.ts

createCookieJar()

function createCookieJar(): CookieJar

Creates an in-memory cookie jar. Returns an object with these methods:

Method Signature Description
getCookies (url: string) => Cookie[] Returns cookies matching URL (domain, secure, expiry)
setCookie (cookie: Cookie, url: string) => void Stores/updates a cookie; auto-fills domain/path
removeCookie (name: string, domain: string, path: string) => void Removes a specific cookie
clear () => void Removes all cookies
getAll () => Cookie[] Returns all stored cookies

parseSetCookie(header)

function parseSetCookie(header: string): Cookie | null

Parses a Set-Cookie header value into a Cookie object. Handles: name=value, Domain, Path, Expires, Max-Age, Secure, HttpOnly, SameSite. Returns null on invalid input.

parseSetCookie('session=abc123; Domain=example.com; Path=/; Secure; HttpOnly; SameSite=Lax');
//   { name: 'session', value: 'abc123', domain: 'example.com', path: '/', secure: true, httpOnly: true, sameSite: 'Lax' }

serializeCookie(cookie)

function serializeCookie(cookie: Cookie): string

Serializes a Cookie to "name=value" (URI-encoded).

serializeCookies(cookies)

function serializeCookies(cookies: Cookie[]): string

Serializes multiple cookies joined by "; ".


Parser Functions

Source: src/parsers/index.ts

Pre-built Parsers

Parser Description
jsonParser res.json()
textParser res.text()
blobParser res.blob()
arrayBufferParser res.arrayBuffer()
formDataParser res.formData()
streamParser res.body (ReadableStream)
bytesParser new Uint8Array(await res.arrayBuffer())
noopParser undefined
import { jsonParser, textParser, bytesParser } from 'fetch-the-wrap';

// Use a specific parser
const res = await fetchTheWrap.get('/data', { parser: textParser });
// res.data will be a string

validateParser<T>(validator)

function validateParser<T>(validator: (data: unknown) => T | Promise<T>): BodyParser<T>

Returns a parser that calls res.json() then passes the result through a validator (e.g., Zod schema parser).

import { z } from 'zod';
import { validateParser } from 'fetch-the-wrap';

const UserSchema = z.object({ id: z.number(), name: z.string() });

const res = await fetchTheWrap.get('/user/1', {
  parser: validateParser(data => UserSchema.parse(data)),
});
// res.data is typed as { id: number; name: string }

createTypedParser<T>()

function createTypedParser<T>(): BodyParser<T>

Returns jsonParser cast to BodyParser<T> for type inference only. No runtime validation.

safeParse<T>(parser)

function safeParse<T>(parser: BodyParser<T>): BodyParser<{ success: true; data: T } | { success: false; error: Error }>

Wraps any parser to return a discriminated result tuple — never throws.

const parsed = await safeParse(jsonParser)(res.raw);
if (parsed.success) {
  console.log(parsed.data);
} else {
  console.error(parsed.error);
}

Plugin Functions

GraphQL — src/plugins/graphql.ts

graphql(client, url, options)

function graphql(client: ClientInstance, url: string, options: GraphQLOptions): Promise<InternalResponse>

Sends a GraphQL POST request with { query, variables, operationName } JSON body.

import { graphql } from 'fetch-the-wrap';
const res = await graphql(client, 'https://api.example.com/graphql', {
  query: `query { user(id: "1") { name } }`,
  variables: {},
});

createGraphQLClient(client, defaultUrl?)

function createGraphQLClient(client: ClientInstance, defaultUrl?: string): { query, mutate }

Returns { query, mutate } helper object. Both accept (query, variables?, options?) and call graphql() internally.

import { createClient, createGraphQLClient } from 'fetch-the-wrap';

const client = createClient();
const gql = createGraphQLClient(client, 'https://api.example.com/graphql');

const res = await gql.query(`
  query GetUser($id: ID!) { user(id: $id) { name email } }
`, { id: '1' });

const res2 = await gql.mutate(`
  mutation UpdateUser($id: ID!, $name: String!) { updateUser(id: $id, name: $name) { id } }
`, { id: '1', name: 'Alice' });

SSE / EventSource — src/plugins/sse.ts

createEventSource(url, request, options)

async function createEventSource(
  url: string,
  request: (config: InternalRequestConfig) => Promise<Response>,
  options?: SSEOptions
): Promise<void>

Connects to an SSE endpoint via GET request with accept: text/event-stream. Parses the event stream according to the SSE spec, calling onMessage for each event, onOpen on connection, onError on errors. Supports lastEventId for reconnection.

interface SSEEvent {
  id: string | null;
  event: string;
  data: string;
  retry: number | null;
}

interface SSEOptions {
  signal?: AbortSignal;
  onMessage?: (event: SSEEvent) => void;
  onError?: (error: Error) => void;
  onOpen?: () => void;
  headers?: Record<string, string>;
  lastEventId?: string;
}
import { createClient, createEventSource } from 'fetch-the-wrap';

const client = createClient({ baseURL: 'https://api.example.com' });

const controller = new AbortController();
await createEventSource('/events', client, {
  signal: controller.signal,
  onOpen: () => console.log('SSE connected'),
  onMessage: (event) => console.log(event.event, event.data),
  onError: (err) => console.error('SSE error:', err),
});

Mock — src/plugins/mock.ts

mock(config)

function mock(config: {
  method?: string;
  url?: string | RegExp;
  response: Partial<InternalResponse> | ((config: InternalRequestConfig) => Partial<InternalResponse>);
  times?: number;
}): () => void

Registers a mock rule. response can be a Partial<InternalResponse> object or a function (config) => Partial<InternalResponse>. Returns a cleanup function. Overrides the default adapter on first call.

import { mock } from 'fetch-the-wrap';

// Mock a specific endpoint (returns cleanup fn)
const unmock = mock({
  method: 'GET',
  url: '/users/1',
  response: {
    status: 200,
    body: JSON.stringify({ id: 1, name: 'Test' }),
    headers: { 'content-type': 'application/json' },
  },
  times: 1, // auto-cleanup after 1 call
});

await fetchTheWrap.get('/users/1'); // returns mocked response
unmock(); // manually restore

// Dynamic response
mock({
  method: 'POST',
  url: '/users',
  response: (config) => ({
    status: 201,
    body: JSON.stringify({ id: 2, ...JSON.parse(config.body as string) }),
  }),
});

mockAll()

function mockAll(): void

Mocks all requests. No rules pass through to the real adapter.

mockRestore()

function mockRestore(): void

Restores the original adapter and clears all mock rules.

resetMocks()

function resetMocks(): void

Clears all mock rules without restoring the adapter.


Type Exports

All types are re-exported from src/index.ts:

From src/types/request.ts

Type Description
HttpMethod Union of all HTTP method literals
Method Uppercase HTTP method
RequestBody Fetch-compatible body types
RequestBodySerializable Serializable body values
QueryParams String, URLSearchParams, record, or tuple array
QueryParamValue Primitive or array of primitives
HeaderValue String, string[], number, boolean
HeadersInit Record or HeaderValue
RedirectPolicy 'follow' | 'manual' | 'error'
BodyType 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData' | 'stream' | 'bytes' | 'raw'
Priority 'high' | 'low' | 'auto'
AuthConfig Username/password/token auth
ProxyConfig HTTP/SOCKS proxy config
TimeoutConfig { request, connection?, idle? }
RetryableStatus Number or pattern like '5xx'

From src/types/response.ts

Type Description
ResponseMeta Response metadata (url, status, headers, timings)
ResponseTimings Phase timing breakdown
RedirectEntry Redirect history entry
ProgressInfo Upload/download progress info

From src/types/options.ts

Type Description
BodyParser<T> (response: Response) => Promise<T>
RequestOptionsBase Per-request options
RetryOptions Retry configuration
CacheOptions Cache configuration
CacheAdapter Pluggable cache store interface
CacheEntry Cached response entry
DedupOptions Deduplication configuration
CircuitBreakerOptions Circuit breaker configuration
ThrottleOptions Rate limiting configuration
ProgressOptions Progress event configuration
CookieOptions Cookie configuration
CookieJar Cookie jar interface
Cookie Cookie object
TracingOptions Tracing configuration
MetricsCollector Metrics collection interface
MetricsSummary Metrics summary
Hooks Lifecycle hooks
InternalRequestConfig Normalized internal request config
InternalResponse Normalized internal response
FetchTheWrapError (type) Error shape
MiddlewareFn (next, config) => Promise<InternalResponse>
ClientInstance Return type of createClient
ClientOptions Client configuration
RequestAdapter (config) => Promise<InternalResponse>
FetchTheWrapResponse<T> Response with data and parsing methods

Architecture Summary

src/
├── core/
│   ├── client.ts      # createClient() — main entry point
│   ├── constants.ts   # HTTP methods, defaults, content-types, header names
│   └── errors.ts      # 9 error classes
├── types/
│   ├── request.ts     # Request type definitions
│   ├── response.ts    # Response type definitions
│   ├── options.ts     # All option/configuration interfaces
│   └── index.ts       # Type re-exports
├── utils/
│   ├── headers.ts     # Header normalization, merge, content-type parsing
│   ├── url.ts         # URL resolution, query serialization
│   ├── body.ts        # Body serialization, parsing, streaming
│   └── merge.ts       # Deep merge, pick, omit, clone
├── adapters/
│   └── adapter.ts     # Browser/Node adapter detection, get/setDefaultAdapter
├── middleware/
│   ├── index.ts       # composeMiddleware, applyHooks
│   ├── retry.ts       # retryMiddleware
│   ├── timeout.ts     # timeoutMiddleware
│   ├── cache.ts       # cacheMiddleware
│   ├── dedup.ts       # dedupMiddleware, clearDedupCache
│   ├── circuit.ts     # circuitBreakerMiddleware, resetAllCircuits
│   ├── throttle.ts    # throttleMiddleware, resetThrottle
│   ├── progress.ts    # progressMiddleware
│   ├── cookies.ts     # cookieMiddleware
│   └── tracing.ts     # tracingMiddleware
├── cookies/
│   └── jar.ts         # createCookieJar, parseSetCookie, serializeCookie, serializeCookies
├── parsers/
│   └── index.ts       # 8 parsers, validateParser, safeParse
├── plugins/
│   ├── graphql.ts     # graphql, createGraphQLClient
│   ├── sse.ts         # createEventSource
│   └── mock.ts        # mock, mockAll, mockRestore, resetMocks
└── index.ts           # Barrel exports, fetchTheWrap singleton

Testing

npm test               # Run all tests (56+ tests)
npm run test:watch     # Watch mode
npm run test:coverage  # With coverage
npm run lint           # TypeScript type check

License

MIT

About

Feature rich NodeJS/Fetch wrapper

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages