@d1g1tal/transportr
v5.2.1
Published
JavaScript wrapper for the Fetch API and more...
Downloads
1,565
Maintainers
Readme
transportr
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,
AbortControllerintegration, andabortAll()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 hooks —
beforeRequest,afterResponse,beforeErrorhooks 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) asAsyncIterable - Progress tracking — Download and upload progress callbacks with loaded/total/percentage
- Safe results —
unwrap: falseoption returnsResult<T>tuples instead of throwing - Concurrent helpers —
Transportr.all()andTransportr.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 markupsanitizePreset: '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-completeRegister 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 callFeature 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
AbortControllersupport jsdomis an optional peer dependency — only needed for HTML/XML/DOM features in Node.js. Install it separately if you usegetHtml(),getXml(),getHtmlFragment(),getScript(),getStylesheet(), orgetImage()in a non-browser environment:
pnpm add jsdomInstallation
# With pnpm:
pnpm add @d1g1tal/transportr
# Or with npm:
npm install @d1g1tal/transportr
# Or with yarn
yarn add @d1g1tal/transportrQuick 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-8Accept: application/json; charset=utf-8timeout: 30 000 mscache:no-storecredentials:same-originmode: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 responses —
getEventStream()returns anAsyncIterable<ServerSentEvent>for SSE endpoints;getJsonStream<T>()returns anAsyncIterable<T>for NDJSON feeds. - Progress tracking —
onDownloadProgressandonUploadProgresscallbacks in request options provide{ loaded, total, percentage }updates. - Safe results — Pass
unwrap: falseper-request or in the constructor to getResult<T>tuples ([true, data]or[false, error]) instead of thrown errors. - Concurrent helpers —
Transportr.all()for parallel requests;Transportr.race()races requests and auto-aborts the losers. - Typed events —
register()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 chainingEvent lifecycle: configured → success | error | aborted | timeout → complete → all-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);
});