A production-grade HTTP request library for TypeScript. Fast, tiny, fully typed — Fetch evolved.
npm install fetch-the-wrap
- Why FetchTheWrap?
- Quick Start
- API Reference
- Error Classes
- Constants
- Header Utilities
- URL Utilities
- Body Utilities
- Merge Utilities
- Adapter Functions
- Middleware System
- Middleware Factories
- Cookie Functions
- Parser Functions
- Plugin Functions
- Type Exports
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
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' });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 },
});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();
});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();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);
}
}Source: src/core/constants.ts
| 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] |
| 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 } |
| 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' |
| 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' |
| Constant | Value |
|---|---|
USER_AGENT |
'fetch-the-wrap/0.1.0' |
Source: src/utils/headers.ts
function normalizeHeaderName(name: string): stringLowercases and trims a header name. Uses an LRU cache (max 1000 entries) to avoid repeated string operations on hot paths.
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' }function mergeHeaders(...sources: (HeadersInit | undefined)[]): Record<string, string>Merges multiple header sources left-to-right. Later sources override earlier ones.
function getHeader(headers: Record<string, string>, name: string): string | undefinedCase-insensitive header read.
function setHeader(headers: Record<string, string>, name: string, value: HeaderValue): voidCase-insensitive header write. Array values are joined with ', '. undefined/null values remove the header.
function hasHeader(headers: Record<string, string>, name: string): booleanCase-insensitive header existence check.
function removeHeader(headers: Record<string, string>, name: string): voidCase-insensitive header removal.
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" } }.
function inferContentType(body: unknown): string | undefinedAuto-detects content-type from body type:
FormDatamultipart/form-dataURLSearchParamsapplication/x-www-form-urlencodedBlobblob's own typestringtext/plainArrayBuffer/ArrayBufferViewapplication/octet-streamobjectapplication/json
function isJSONContentType(contentType: string): booleanChecks if a content-type string is application/json or application/*+json.
Source: src/utils/url.ts
function serializeQueryParams(params: QueryParams | undefined): stringSerializes 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'function resolveURL(baseURL: string | undefined, url: string, params?: QueryParams): stringResolves 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'function isAbsoluteURL(url: string): booleanReturns true if URL has a protocol scheme or starts with //.
function parseQueryString(query: string): Record<string, string>Decodes a query string into a record. Handles leading ?, + for spaces.
function buildURL(parts: { protocol?, host?, port?, path?, params? }): stringConstructs a full URL from components. Adds :// after protocol, : before port, / before path if missing, appends query string.
Source: src/utils/body.ts
function serializeBody(body: unknown, headers: Record<string, string>): RequestBodySerializes 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.
function isBodySerializable(value: unknown): value is RequestBodySerializableType guard checking if a value can be serialized (string, number, boolean, null, plain object, array of serializables).
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.
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');
}function createFormData(data: Record<string, unknown>, form?: FormData): FormDataCreates or appends to a FormData from a record. Handles Blob values (file upload), arrays (repeated keys), and primitives (toString).
Source: src/utils/merge.ts
function deepMerge<T = Record<string, unknown>>(target, ...sources): TRecursively 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 } }function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>Returns a new object with only the specified keys.
function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K>Returns a new object without the specified keys.
function cloneDeep<T>(value: T): TDeep clones a value. Handles Date, RegExp, Map, Set, Blob, ArrayBuffer, URLSearchParams, FormData by reference (they're immutable or have their own clone).
Source: src/adapters/adapter.ts
function detectAdapter(): RequestAdapterAuto-detects the runtime environment. Returns browser adapter if globalThis.fetch exists, otherwise Node adapter.
function getDefaultAdapter(): RequestAdapterReturns the cached default adapter. Lazily initializes via detectAdapter() on first call.
function setDefaultAdapter(adapter: RequestAdapter): voidOverrides the default adapter. Used by the mock plugin and for custom adapters.
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,
};
});Source: src/middleware/index.ts
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);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.
function applyHooks(config: InternalRequestConfig): InternalRequestConfigConverts lifecycle hooks (beforeRequest, afterResponse, onError) into MiddlewareFn entries and appends them to config.middleware. Called during request config construction.
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)= 3000msexponential:1000 * 2^2= 4000msdecorrelated: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;
},
}));Source: src/middleware/timeout.ts:9
function timeoutMiddleware(timeout?: number | TimeoutConfig): MiddlewareFnCreates 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
}));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'),
}));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 }));function clearDedupCache(): voidClears all in-flight dedup promises. Source: dedup.ts:63.
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.
function resetAllCircuits(): voidClears all circuit breaker states. Source: circuit.ts:68.
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) |
function resetThrottle(): voidClears all throttle states. Source: throttle.ts:67.
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,
}));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 cookieSource: 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
Source: src/cookies/jar.ts
function createCookieJar(): CookieJarCreates 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 |
function parseSetCookie(header: string): Cookie | nullParses 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' }function serializeCookie(cookie: Cookie): stringSerializes a Cookie to "name=value" (URI-encoded).
function serializeCookies(cookies: Cookie[]): stringSerializes multiple cookies joined by "; ".
Source: src/parsers/index.ts
| 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 stringfunction 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 }function createTypedParser<T>(): BodyParser<T>Returns jsonParser cast to BodyParser<T> for type inference only. No runtime validation.
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);
}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: {},
});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' });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),
});function mock(config: {
method?: string;
url?: string | RegExp;
response: Partial<InternalResponse> | ((config: InternalRequestConfig) => Partial<InternalResponse>);
times?: number;
}): () => voidRegisters 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) }),
}),
});function mockAll(): voidMocks all requests. No rules pass through to the real adapter.
function mockRestore(): voidRestores the original adapter and clears all mock rules.
function resetMocks(): voidClears all mock rules without restoring the adapter.
All types are re-exported from src/index.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' |
| Type | Description |
|---|---|
ResponseMeta |
Response metadata (url, status, headers, timings) |
ResponseTimings |
Phase timing breakdown |
RedirectEntry |
Redirect history entry |
ProgressInfo |
Upload/download progress info |
| 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 |
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
npm test # Run all tests (56+ tests)
npm run test:watch # Watch mode
npm run test:coverage # With coverage
npm run lint # TypeScript type checkMIT