npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@d1g1tal/transportr

v5.2.1

Published

JavaScript wrapper for the Fetch API and more...

Downloads

1,565

Readme

transportr

npm version npm downloads CI codecov License: MIT Node.js TypeScript

A TypeScript Fetch API wrapper providing type-safe HTTP requests with advanced abort/timeout handling, event-driven architecture, and automatic content-type based response processing.

Features

  • Type-safe — Full TypeScript support with strict types, branded JSON strings, and typed headers
  • Automatic response handling — Content-type based response parsing (JSON, HTML, XML, images, streams, etc.)
  • Abort & timeout management — Per-request timeouts, AbortController integration, and abortAll() for cleanup
  • Event-driven — Global and instance-level lifecycle events (configured, success, error, complete, etc.)
  • Retry logic — Configurable retry with exponential backoff, status code filtering, and method filtering
  • Request deduplication — Identical in-flight GET/HEAD requests share a single fetch
  • Lifecycle hooksbeforeRequest, afterResponse, beforeError hooks at global, instance, and per-request levels
  • XSRF/CSRF protection — Automatic cookie-to-header token injection
  • HTML selectors — Extract specific elements from HTML responses with CSS selectors
  • FormData auto-detection — Automatically handles FormData, Blob, ArrayBuffer, and stream bodies
  • Streaming — SSE (getEventStream) and NDJSON (getJsonStream) as AsyncIterable
  • Progress tracking — Download and upload progress callbacks with loaded/total/percentage
  • Safe resultsunwrap: false option returns Result<T> tuples instead of throwing
  • Concurrent helpersTransportr.all() and Transportr.race() with auto-abort for race losers

Why Transportr?

The HTTP client space in JavaScript/TypeScript is crowded. Here's where Transportr fits and why it may be the right choice for your project.

The Competition at a Glance

| Library | Minified | Gzipped | Engine | Request API | Philosophy | |---------|----------|---------|--------|-------------|------------| | axios | 35.4 kB | 13.9 kB | ≥ 10 | XMLHttpRequest / http | Kitchen-sink, XHR-based | | ky | 13.7 kB | 4.9 kB | ≥ 18 | Fetch | Tiny fetch wrapper, browser-first | | ofetch | 9.3 kB | 3.8 kB | ≥ 18 | Fetch | Universal, minimal API | | wretch | 4.8 kB | 1.9 kB | ≥ 14 | Fetch | Fluent-chain, middleware-based | | got | — | ~43 kB | ≥ 18 | http/https | Node-only, feature-rich | | transportr | 27 kB | 8.5 kB* | ≥ 22 | Fetch | Content-type-aware, event-driven |

Sizes from Bundlephobia. *Transportr bundles @d1g1tal/media-type, @d1g1tal/subscribr, and DOMPurify. Optional jsdom peer dependency is not included — only needed for HTML/XML/DOM features in Node.js.

What Every Fetch Wrapper Gives You

All of these libraries wrap fetch and add roughly the same core set of features:

  • JSON body serialization + parsing
  • Error on non-2xx responses
  • Request timeout
  • Base URL configuration
  • TypeScript types

If you only need those basics, any of them will do. ky at 3.5 kB is a perfectly fine choice.

What Only Transportr Gives You

1. Content-Type-Aware Response Handling

No other HTTP client knows what it fetched and processes it accordingly. Transportr maps response Content-Type directly to strongly-typed return values:

const api = new Transportr('https://example.com');

// Returns Promise<Json> — parsed and typed
const data = await api.getJson('/api/users');

// Returns Promise<Document> — fully parsed, DOMPurify-sanitized HTML
const page = await api.getHtml('/page');

// Returns Promise<Document> — sanitized and parsed XML
const feed = await api.getXml('/feed.xml');

// Returns Promise<DocumentFragment> — isolated fragment, no full document
const fragment = await api.getHtmlFragment('/partial');

// Returns Promise<void> — script fetched, verified, injected into <head>
await api.getScript('https://cdn.example.com/widget.js');

// Returns Promise<void> — stylesheet fetched and injected into <head>
await api.getStylesheet('https://cdn.example.com/theme.css');

// Returns Promise<HTMLImageElement> — decoded, memory-safe
const img = await api.getImage('/assets/photo.webp');

// Returns Promise<ReadableStream> — for large payloads
const stream = await api.getStream('/export/data.csv');

The type system enforces what you get back. You can't accidentally call .querySelector() on a JSON response.

2. Automatic DOMPurify Sanitization

getHtml(), getXml(), and getHtmlFragment() sanitize the response through DOMPurify before parsing. You don't have to remember to sanitize — it's built into the transport layer.

This is the correct place to sanitize: as close to the network boundary as possible, before the content ever reaches a parser or your application code.

getHtml()/getXml() sanitize and parse the full document, preserving <head> content (<title>, <meta>, <link>, <style>, etc.) alongside <body>. getHtmlFragment() sanitizes and parses only the given markup as a DocumentFragment — any document-wrapper elements (<html>, <head>, <body>) in the response are ignored.

Use sanitizePreset to pick a common behavior without learning DOMPurify internals:

// Default behavior (safe): strict DOMPurify sanitization
await api.getHtml('/page', { sanitizePreset: 'strict' });

// Balanced preset for common compatibility cases
await api.getHtmlFragment('/partial', { sanitizePreset: 'balanced' });

// Relaxed preset for more permissive legacy-compatible markup
await api.getHtmlFragment('/partial', { sanitizePreset: 'relaxed' });

// Trusted content only: bypass sanitization entirely
await api.getHtmlFragment('/legacy-template', { sanitizePreset: 'bypass' });

When you need to preserve inert template scripts without disabling sanitization for everything else, use sanitization instead of bypass:

// Preserve common inert template script types such as text/x-jquery-tmpl
await api.getHtmlFragment('/legacy-template', {
	sanitization: {
		preserveTemplateScripts: true
	}
});

// Preserve an application-specific inert script type as well
await api.getHtmlFragment('/legacy-template', {
	sanitization: {
		preserveTemplateScripts: true,
		templateScriptTypes: [ 'text/x-my-template' ]
	}
});

Use sanitizePreset: 'bypass' only when the entire response is trusted and you intentionally want to keep executable scripts, inline handlers, and javascript: URLs.

When you need finer control, sanitization also supports granular allowances:

await api.getHtml('/page', {
	sanitization: {
		allowScripts: true,
		allowStyles: true
	}
});

allowScripts preserves JavaScript constructs while still letting DOMPurify sanitize everything else. That includes:

  • <script> tags, including template script tags
  • inline event handlers such as onclick
  • javascript: URLs

allowStyles separately preserves stylesheet markup:

  • <link rel="stylesheet"> tags only
  • <style> tags
  • inline style="..." attributes

Important: allowStyles preserves raw CSS. It does not sanitize CSS inside <style> tags or CSS declarations inside inline style attributes.

Use allowScripts when you want to preserve JavaScript constructs but still keep DOMPurify active for unrelated markup.

Use sanitizePreset: 'bypass' only when you want to skip sanitization entirely.

In short:

  • allowScripts: preserve JavaScript, still sanitize unrelated markup
  • sanitizePreset: 'bypass': preserve everything, sanitize nothing

3. HTML Selector Extraction

Fetch a page and get back exactly the element you want, not the whole document:

// Returns Promise<Element | null>
const nav = await api.getHtml('/page', {}, 'nav.main');
const price = await api.getHtmlFragment('/product', {}, '.price');

Useful for partial page loading, widget hydration, and scraping structured content without a separate HTML parsing step.

4. Script & Stylesheet Injection with Cleanup

getScript() and getStylesheet() use URL.createObjectURL() to inject remote assets, then automatically revoke the object URL after load/error. No memory leaks, no dangling blob URLs:

// Loads, verifies, injects, and cleans up automatically
await api.getScript('https://partner.example.com/sdk.js');
await api.getStylesheet('https://cdn.example.com/theme.css');

This is a real pattern for micro-frontend loaders and dynamic plugin systems. No other HTTP client handles it.

5. Full Lifecycle Event System

Transportr has a two-tier event system (global + per-instance) with a defined lifecycle:

configured → success | error | aborted | timeout → complete → all-complete

Register handlers globally (across all instances) or per-instance:

// All requests, all instances
Transportr.register(Transportr.RequestEvent.SUCCESS, (event, data) => {
	analytics.track('api_success', { url: data.url });
});

// Just this instance
api.register(Transportr.RequestEvent.TIMEOUT, (event, error) => {
	toast.error('Request timed out');
});

ky has hooks. ofetch has hooks. Neither has a persistent, named event system you can subscribe to and unsubscribe from independently.

6. Request Deduplication

Identical in-flight GET/HEAD requests share a single fetch. Each caller receives a cloned Response:

// Only one network request — both get independent Response clones
const [a, b] = await Promise.all([
	api.get('/config', { dedupe: true }),
	api.get('/config', { dedupe: true }),
]);

This is commonly left to userland caching or state management. Transportr makes it a first-class option.

7. abortAll() for Clean Teardown

Cancel every in-flight request across all instances in one call:

// Route change, unmount, logout — kill everything
Transportr.abortAll();

Useful in SPAs for route transitions or session expiry. ky and ofetch require manual AbortController management per-request.

8. Structured Hook Layers

Hooks run in a deterministic order: global → instance → per-request. This lets you separate concerns cleanly:

Transportr.addHooks({ beforeRequest: [addRequestId] });  // Always runs
api.addHooks({ afterResponse: [logLatency] });            // Runs for this API
await api.get('/data', { hooks: { beforeError: [notify] } }); // Only this call

Feature Comparison

| Feature | transportr | ky | ofetch | wretch | axios | |---------|:----------:|:--:|:------:|:------:|:-----:| | JSON request/response | ✅ | ✅ | ✅ | ✅ | ✅ | | TypeScript (first-class) | ✅ | ✅ | ✅ | ✅ | ✅ | | Timeout | ✅ | ✅ | ✅ | ✅ | ✅ | | Retry with backoff | ✅ | ✅ | ✅ | ✅ | ⚠️ plugin | | SSE / NDJSON streaming | ✅ | ❌ | ❌ | ❌ | ❌ | | Download / upload progress | ✅ | ✅ | ❌ | ⚠️ addon | ✅ | | Safe result tuples | ✅ | ❌ | ❌ | ❌ | ❌ | | Concurrent helpers with auto-abort | ✅ | ❌ | ❌ | ❌ | ❌ | | Request deduplication | ✅ | ❌ | ❌ | ⚠️ middleware | ❌ | | Abort all in-flight | ✅ | ❌ | ❌ | ❌ | ❌ | | Lifecycle event system | ✅ | ❌ | ❌ | ❌ | ❌ | | HTML response → Document | ✅ | ❌ | ❌ | ❌ | ❌ | | XML response → Document | ✅ | ❌ | ❌ | ❌ | ❌ | | HTML fragment with selector | ✅ | ❌ | ❌ | ❌ | ❌ | | Auto DOMPurify sanitization | ✅ | ❌ | ❌ | ❌ | ❌ | | Script injection + cleanup | ✅ | ❌ | ❌ | ❌ | ❌ | | Stylesheet injection + cleanup | ✅ | ❌ | ❌ | ❌ | ❌ | | Image → HTMLImageElement | ✅ | ❌ | ❌ | ❌ | ❌ | | XSRF/CSRF protection | ✅ | ❌ | ❌ | ❌ | ✅ | | beforeRequest hooks | ✅ | ✅ | ✅ | ✅ | ✅ | | afterResponse hooks | ✅ | ✅ | ✅ | ✅ | ✅ | | Global + instance hook layers | ✅ | ❌ | ❌ | ❌ | ❌ | | Branded JSON types | ✅ | ❌ | ❌ | ❌ | ❌ | | Custom content-type handlers | ✅ | ❌ | ❌ | ❌ | ❌ |

When to Choose Transportr

Choose Transportr if you are building:

  • A micro-frontend loader that fetches and injects remote scripts or stylesheets
  • An SSR/ISR application that fetches HTML partials and extracts fragments server-side
  • A content aggregator that deals with mixed response types (JSON, HTML, XML, images)
  • A dashboard or scraper that parses HTML with CSS selectors and needs built-in sanitization
  • An application where abort-all on route change or request deduplication are requirements
  • A project that wants typed lifecycle events rather than ad-hoc error handling

Choose ky or ofetch if you are building:

  • A pure JSON API client where bundle size is the primary constraint
  • A project that has no DOM-manipulation requirements
  • A project that already has its own event/observability layer

Requirements

  • Node.js ≥ 22.0.0 or a modern browser with native Fetch API and AbortController support
  • jsdom is an optional peer dependency — only needed for HTML/XML/DOM features in Node.js. Install it separately if you use getHtml(), getXml(), getHtmlFragment(), getScript(), getStylesheet(), or getImage() in a non-browser environment:
pnpm add jsdom

Installation

# With pnpm:
pnpm add @d1g1tal/transportr

# Or with npm:
npm install @d1g1tal/transportr

# Or with yarn
yarn add @d1g1tal/transportr

Quick Start

Only the main module is required — the submodule constants (RequestHeader, ContentType, etc.) are optional conveniences. Anywhere a constant is used, a plain string works just as well.

Out of the box, every instance defaults to:

  • Content-Type: application/json; charset=utf-8
  • Accept: application/json; charset=utf-8
  • timeout: 30 000 ms
  • cache: no-store
  • credentials: same-origin
  • mode: cors
import { Transportr } from '@d1g1tal/transportr';

const api = new Transportr('https://api.example.com');

// GET JSON — default Accept header is already application/json
const data = await api.getJson('/users/1');

// POST with JSON body — automatically serialized, no Content-Type needed
const created = await api.post('/users', { body: { name: 'Alice' } });

// GET with search params
const results = await api.getJson('/search', { searchParams: { q: 'term', page: 1 } });

// Typed response using generics
interface User { id: number; name: string; }
const user = await api.get<User>('/users/1');

// Plain strings work anywhere — constants are just for convenience
const api2 = new Transportr('https://api.example.com', {
	headers: { 'authorization': 'Bearer token', 'accept-language': 'en-US' }
});

Browser / CDN Usage

The package is published as pure ESM and works directly in modern browsers — no bundler required. All dependencies (@d1g1tal/media-type, @d1g1tal/subscribr, DOMPurify) are bundled into the output, so there are no external module URLs to manage. jsdom is not needed in a browser environment.

With an import map (recommended)

An import map mirrors the package's named submodule exports and keeps your code identical to the Node.js form — use bare specifiers exactly as you would in a bundled project:

<script type="importmap">
{
  "imports": {
    "@d1g1tal/transportr": "https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/transportr.js",
    "@d1g1tal/transportr/request-header": "https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/request-header.js",
    "@d1g1tal/transportr/request-method": "https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/request-method.js",
    "@d1g1tal/transportr/content-type": "https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/content-type.js",
    "@d1g1tal/transportr/response-header": "https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/response-header.js"
  }
}
</script>

<script type="module">
	import { Transportr } from '@d1g1tal/transportr';
	import { RequestHeader } from '@d1g1tal/transportr/request-header';
	import { RequestMethod } from '@d1g1tal/transportr/request-method';
	import { ContentType } from '@d1g1tal/transportr/content-type';
	import { ResponseHeader } from '@d1g1tal/transportr/response-header';

	const api = new Transportr('https://api.example.com', {
		headers: {
			[RequestHeader.AUTHORIZATION]: 'Bearer token',
			[RequestHeader.ACCEPT]: ContentType.JSON
		}
	});

	const data = await api.getJson('/users/1');
	console.log(data);
</script>

Without an import map

The CDN resolves the "." entry in exports automatically, so no explicit file path is needed for the main module. Submodules use their CDN paths directly:

<script type="module">
	import { Transportr } from 'https://cdn.jsdelivr.net/npm/@d1g1tal/transportr';
	import { RequestHeader } from 'https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/request-header.js';
	import { ContentType } from 'https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/content-type.js';

	const api = new Transportr('https://api.example.com', {
		headers: {
			[RequestHeader.AUTHORIZATION]: 'Bearer token',
			[RequestHeader.ACCEPT]: ContentType.JSON
		}
	});

	const data = await api.getJson('/users/1');
	console.log(data);
</script>

Import map support is available in all browsers covered by this project's browserslist configuration (Chrome 89+, Firefox 108+, Safari 16.4+).

Migrating from v2

v3.0 contains breaking renames plus new features. The renames are all find-and-replace — no behavior changed.

Submodule paths & exported symbols

| Before (v2) | After (v3) | |---|---| | @d1g1tal/transportr/headers | @d1g1tal/transportr/request-header | | @d1g1tal/transportr/methods | @d1g1tal/transportr/request-method | | @d1g1tal/transportr/media-types | @d1g1tal/transportr/content-type | | @d1g1tal/transportr/response-headers | @d1g1tal/transportr/response-header | | HttpRequestHeader | RequestHeader | | HttpRequestMethod | RequestMethod | | HttpMediaType | ContentType | | HttpResponseHeader | ResponseHeader |

Static properties on Transportr

| Before (v2) | After (v3) | |---|---| | Transportr.RequestEvents | Transportr.RequestEvent | | Transportr.RequestModes | Transportr.RequestMode | | Transportr.RequestPriorities | Transportr.RequestPriority | | Transportr.RedirectPolicies | Transportr.RedirectPolicy |

New in v3

  • Streaming responsesgetEventStream() returns an AsyncIterable<ServerSentEvent> for SSE endpoints; getJsonStream<T>() returns an AsyncIterable<T> for NDJSON feeds.
  • Progress trackingonDownloadProgress and onUploadProgress callbacks in request options provide { loaded, total, percentage } updates.
  • Safe results — Pass unwrap: false per-request or in the constructor to get Result<T> tuples ([true, data] or [false, error]) instead of thrown errors.
  • Concurrent helpersTransportr.all() for parallel requests; Transportr.race() races requests and auto-aborts the losers.
  • Typed eventsregister() now narrows the event data type based on the event name.

API

Constructor

new Transportr(url?: URL | string | RequestOptions, options?: RequestOptions)

Creates a new instance. When url is omitted, defaults to globalThis.location.origin.

// With base URL
const api = new Transportr('https://api.example.com/v2');

// With URL and default options
const api = new Transportr('https://api.example.com', {
	timeout: 10000,
	headers: { 'Authorization': 'Bearer token' }
});

// With options only (uses current origin)
const api = new Transportr({ timeout: 5000 });

Updating Instance Options

Update any option that was set at construction time, without creating a new instance. configure() accepts the same shape as the constructor options. Headers and searchParams are merged onto existing defaults; all other options overwrite the current value; hooks are appended.

const api = new Transportr('https://api.example.com', {
	timeout: 30000,
	credentials: 'same-origin'
});

// After login — inject auth token and tighten timeout
api.configure({
	timeout: 10000,
	credentials: 'include',
	headers: { 'Authorization': `Bearer ${token}` }
});

// Add default search params for all subsequent requests
api.configure({ searchParams: { version: '2', locale: 'en' } });

// Chainable
api
	.configure({ timeout: 5000 })
	.configure({ headers: { 'X-Tenant': 'acme' } })
	.addHooks({ beforeRequest: [logHook] });

Request Methods

| Method | Description | |--------|-------------| | get(path?, options?) | GET request with auto content-type handling | | post(path?, options?) | POST request | | put(path?, options?) | PUT request | | patch(path?, options?) | PATCH request | | delete(path?, options?) | DELETE request | | head(path?, options?) | HEAD request | | options(path?, options?) | OPTIONS request (returns allowed methods) | | request(path?, options?) | Raw request returning TypedResponse<T> |

Typed Response Methods

| Method | Returns | Accept Header | |--------|---------|---------------| | getJson(path?, options?) | Json | application/json | | getHtml(path?, options?, selector?) | Document \| Element | text/html | | getHtmlFragment(path?, options?, selector?) | DocumentFragment \| Element | text/html | | getXml(path?, options?) | Document | application/xml | | getScript(path?, options?) | void (injected into DOM) | application/javascript | | getStylesheet(path?, options?) | void (injected into DOM) | text/css | | getBlob(path?, options?) | Blob | application/octet-stream | | getImage(path?, options?) | HTMLImageElement | image/* | | getBuffer(path?, options?) | ArrayBuffer | application/octet-stream | | getStream(path?, options?) | ReadableStream | application/octet-stream | | getEventStream(path?, options?) | AsyncIterable<ServerSentEvent> | text/event-stream | | getJsonStream<T>(path?, options?) | AsyncIterable<T> | application/x-ndjson |

Request Options

type RequestOptions = {
	headers?: RequestHeaders;
	searchParams?: URLSearchParams | string | Record<string, string | number | boolean>;
	timeout?: number;              // Default: 30000ms
	global?: boolean;              // Emit global events (default: true)
	body?: BodyInit | JsonObject;  // Auto-serialized for JSON content-type
	retry?: number | RetryOptions;
	dedupe?: boolean;              // Deduplicate identical GET/HEAD requests
	xsrf?: boolean | XsrfOptions;
	hooks?: HooksOptions;
	unwrap?: boolean;              // false → return Result<T> tuple instead of throwing
	sanitizePreset?: 'strict' | 'balanced' | 'relaxed' | 'bypass';
	sanitization?: {
		preset?: 'strict' | 'balanced' | 'relaxed' | 'bypass';
		preserveTemplateScripts?: boolean;
		templateScriptTypes?: string[];
		allowScripts?: boolean;
		allowStyles?: boolean;
	};
	onDownloadProgress?: (progress: DownloadProgress) => void;
	onUploadProgress?: (progress: DownloadProgress) => void;
	// ...all standard RequestInit properties (cache, credentials, mode, etc.)
};

Retry

// Simple: retry up to 3 times with default settings
await api.get('/data', { retry: 3 });

// Advanced configuration
await api.get('/data', {
	retry: {
		limit: 3,
		statusCodes: [408, 413, 429, 500, 502, 503, 504],
		methods: ['GET', 'PUT', 'HEAD', 'DELETE', 'OPTIONS'],
		delay: 300,        // ms before first retry
		backoffFactor: 2   // exponential backoff multiplier
	}
});

Request Deduplication

When dedupe: true, identical in-flight GET/HEAD requests share a single fetch call. Each consumer receives a cloned response.

// Only one fetch call is made
const [a, b] = await Promise.all([
	api.get('/data', { dedupe: true }),
	api.get('/data', { dedupe: true })
]);

Lifecycle Hooks

Hooks run in order: global → instance → per-request.

// Global hooks (all instances)
Transportr.addHooks({
	beforeRequest: [async (options, url) => {
		options.headers.set('X-Request-ID', crypto.randomUUID());
		return options;
	}],
	afterResponse: [async (response, options) => response],
	beforeError: [(error) => error]
});

// Instance hooks
api.addHooks({
	afterResponse: [async (response) => {
		console.log(`Response: ${response.status}`);
		return response;
	}]
});

// Per-request hooks
await api.get('/data', {
	hooks: { beforeRequest: [async (opts) => opts] }
});

Events

// Global events (all instances)
const reg = Transportr.register(Transportr.RequestEvent.SUCCESS, (event, data) => {
	console.log('Request succeeded:', data);
});

// Instance events
const reg = api.register(Transportr.RequestEvent.ERROR, (event, error) => {
	console.error('Request failed:', error);
});

// Unregister
api.unregister(reg);  // Returns `this` for chaining

Event lifecycle: configuredsuccess | error | aborted | timeoutcompleteall-complete

Additional events: retry (emitted on each retry attempt)

Error Handling

Non-2xx responses throw an error with name === 'HttpError'. Aborted and timed-out requests also produce an HttpError with synthetic status codes.

import type { HttpError } from '@d1g1tal/transportr';

try {
	const user = await api.getJson('/users/1');
} catch (error) {
	if (error instanceof Error && error.name === 'HttpError') {
		const httpError = error as unknown as HttpError;
		console.error(httpError.statusCode);  // HTTP status code
		console.error(httpError.statusText);  // HTTP status text
		console.error(httpError.entity);      // parsed response body (if any)
		console.error(httpError.url?.href);   // request URL
		console.error(httpError.method);      // HTTP method used
		console.error(httpError.timing);      // { start, end, duration } in ms
	}
}

Synthetic status codes for non-HTTP failures:

| Code | Text | Cause | |------|------|-------| | 499 | Aborted | Cancelled via controller.abort() or Transportr.abortAll() | | 504 | Request Timeout | timeout option exceeded |

Abort & Timeout

// Per-request timeout
await api.get('/slow', { timeout: 5000 });

// Manual abort via AbortController
const controller = new AbortController();
api.get('/data', { signal: controller.signal });
controller.abort();

// Abort all in-flight requests
Transportr.abortAll();

XSRF/CSRF Protection

// Default: reads 'XSRF-TOKEN' cookie, sets 'X-XSRF-TOKEN' header
await api.post('/data', { body: payload, xsrf: true });

// Custom cookie/header names
await api.post('/data', {
	body: payload,
	xsrf: { cookieName: 'MY-CSRF', headerName: 'X-MY-CSRF' }
});

Streaming

// Server-Sent Events (SSE)
for await (const event of await api.getEventStream('/chat/completions', { body: { prompt } })) {
	console.log(event.event, event.data);
}

// NDJSON (Newline Delimited JSON)
interface LogEntry { ts: number; message: string; }
for await (const entry of await api.getJsonStream<LogEntry>('/logs/stream')) {
	processEntry(entry);
}

Progress Tracking

// Download progress
await api.getBlob('/large-file', {
	onDownloadProgress: ({ loaded, total, percentage }) => {
		console.log(`${percentage}% (${loaded}/${total})`);
	}
});

// Upload progress
await api.post('/upload', {
	body: largeBlob,
	onUploadProgress: ({ loaded, total, percentage }) => {
		console.log(`Uploading: ${percentage}%`);
	}
});

Safe Results (unwrap: false)

Pass unwrap: false to get a Result<T> tuple instead of thrown errors. The tuple is [true, data] on success or [false, HttpError] on failure.

// Per-request
const [ok, result] = await api.getJson('/users/1', { unwrap: false });
if (ok) {
	console.log(result); // typed as Json
} else {
	console.error(result.statusCode); // typed as HttpError
}

// Constructor-level default
const safeApi = new Transportr('https://api.example.com', { unwrap: false });
const [ok, data] = await safeApi.getJson('/data');

Concurrent Requests

// Run multiple requests in parallel
const [users, posts] = await Transportr.all([
	api.getJson('/users'),
	api.getJson('/posts')
]);

// Race requests — first to settle wins, losers are aborted
const fastest = await Transportr.race([
	(signal) => api.getJson('/primary-cdn/data', { signal }),
	(signal) => api.getJson('/fallback-cdn/data', { signal })
]);

HTML Selector Support

// Get a specific element from HTML response
const nav = await api.getHtml('/page', {}, 'nav.main');
const item = await api.getHtmlFragment('/partial', {}, '.item:first-child');

FormData & Raw Bodies

FormData, Blob, ArrayBuffer, ReadableStream, TypedArray, and URLSearchParams are sent as-is. The Content-Type header is automatically removed so the runtime can set it (e.g., multipart boundary for FormData).

const form = new FormData();
form.append('file', fileBlob, 'photo.jpg');
await api.post('/upload', { body: form });

Custom Content-Type Handlers

// Register a custom handler (takes priority over built-in)
Transportr.registerContentTypeHandler('csv', async (response) => {
	const text = await response.text();
	return text.split('\n').map(row => row.split(','));
});

// Remove a handler
Transportr.unregisterContentTypeHandler('csv');

Cleanup

// Tear down a single instance
api.destroy();

// Tear down all global state
Transportr.unregisterAll();

// Clear only global hooks without aborting in-flight requests
Transportr.clearHooks();

Method Chaining

Instance methods configure(), unregister(), addHooks(), and clearHooks() return this:

api
	.configure({ timeout: 5000, credentials: 'include' })
	.configure({ headers: { 'Authorization': `Bearer ${token}` } })
	.addHooks({ beforeRequest: [myHook] })
	.clearHooks()
	.addHooks({ afterResponse: [logHook] });

Instance Properties

| Property | Type | Description | |----------|------|-------------| | baseUrl | URL | The base URL used for all requests from this instance |

Static Properties

| Property | Description | |----------|-------------| | Transportr.CredentialsPolicy | Credentials policy constants | | Transportr.RequestMode | Request mode constants | | Transportr.RequestPriority | Request priority constants | | Transportr.RedirectPolicy | Redirect policy constants | | Transportr.ReferrerPolicy | Referrer policy constants | | Transportr.RequestEvent | Event name constants | | Transportr.all(requests) | Run requests in parallel (Promise.all with tuple typing) | | Transportr.race(requests) | Race request factories; auto-aborts losers | | Transportr.abortAll() | Abort all in-flight requests across all instances | | Transportr.addHooks(hooks) | Add global lifecycle hooks | | Transportr.clearHooks() | Remove all global hooks | | Transportr.register(event, handler) | Register a global event handler | | Transportr.unregister(registration) | Remove a global event handler |

Submodule Imports

HTTP constant objects are available as named submodule imports. Each is a tree-shakeable, side-effect-free object of string constants — useful for avoiding magic strings and getting autocomplete.

@d1g1tal/transportr/request-header

Request header name constants.

import { Transportr } from '@d1g1tal/transportr';
import { RequestHeader } from '@d1g1tal/transportr/request-header';

const api = new Transportr('https://api.example.com', {
	headers: {
		[RequestHeader.AUTHORIZATION]: 'Bearer token',
		[RequestHeader.CONTENT_TYPE]: 'application/json',
		[RequestHeader.ACCEPT_LANGUAGE]: 'en-US'
	}
});

@d1g1tal/transportr/request-method

HTTP method string constants.

import { Transportr } from '@d1g1tal/transportr';
import { RequestMethod } from '@d1g1tal/transportr/request-method';

const api = new Transportr('https://api.example.com');
const response = await api.request('/data', { method: RequestMethod.PATCH });

@d1g1tal/transportr/content-type

MIME type string constants covering common content types (JSON, HTML, XML, CSS, images, audio, video, and more).

import { Transportr } from '@d1g1tal/transportr';
import { RequestHeader } from '@d1g1tal/transportr/request-header';
import { ContentType } from '@d1g1tal/transportr/content-type';

const api = new Transportr('https://api.example.com', {
	headers: { [RequestHeader.ACCEPT]: ContentType.JSON }
});

// Use as a content-type value
const csvData = 'id,name\n1,Alice';
await api.post('/upload', {
	body: csvData,
	headers: { [RequestHeader.CONTENT_TYPE]: ContentType.CSV }
});

@d1g1tal/transportr/response-header

Response header name constants — useful when reading headers from a response.

import { Transportr } from '@d1g1tal/transportr';
import { ResponseHeader } from '@d1g1tal/transportr/response-header';

const api = new Transportr('https://api.example.com');
const reg = api.register(Transportr.RequestEvent.SUCCESS, (event, data) => {
	const response = data as Response;
	const etag = response.headers.get(ResponseHeader.ETAG);
	const retryAfter = response.headers.get(ResponseHeader.RETRY_AFTER);
	const location = response.headers.get(ResponseHeader.LOCATION);
});

License

MIT