HTTP Transport
MeroJs builds on a small, Web-Standards HttpClient that you can also use on
its own — for talking to a node’s endpoints the typed clients don’t cover, or in
a non-Calimero project. It’s built on fetch with zero dependencies.
Create a client
Section titled “Create a client”Pick the factory for your runtime:
import { createBrowserHttpClient, createNodeHttpClient, createUniversalHttpClient,} from '@calimero-network/mero-js';
const http = createBrowserHttpClient({ baseUrl: 'https://api.calimero.network', getAuthToken: async () => localStorage.getItem('access_token') ?? undefined, onTokenRefresh: async (t) => localStorage.setItem('access_token', t),});
const data = await http.get<{ message: string }>('/api/hello');All three take the same options:
interface HttpClientOptions { baseUrl: string; fetch?: typeof fetch; // node & universal getAuthToken?: () => Promise<string | undefined>; // attach a bearer token onTokenRefresh?: (newToken: string) => Promise<void>; refreshToken?: () => Promise<string>; // called on 401 token_expired onAuthRevoked?: () => Promise<void> | void; // token_reuse / token_revoked defaultHeaders?: Record<string, string>; timeoutMs?: number; // default 30000 credentials?: RequestCredentials; defaultAbortSignal?: AbortSignal;}Methods
Section titled “Methods”http.get<T>(path, init?);http.post<T>(path, body?, init?);http.put<T>(path, body?, init?);http.delete<T>(path, init?);http.patch<T>(path, body?, init?);http.head(path, init?); // → { headers, status }http.request<T>(path, init?); // genericinit is a standard RequestInit plus two extras:
interface RequestOptions extends RequestInit { parse?: 'json' | 'text' | 'blob' | 'arrayBuffer' | 'response'; timeoutMs?: number;}Response parsing
Section titled “Response parsing”By default the client picks a parser from Content-Type:
application/json→jsontext/*→text- anything else →
arrayBuffer
Override per call, or ask for the raw Response:
const json = await http.get('/api/data', { parse: 'json' });const text = await http.get('/api/readme', { parse: 'text' });const blob = await http.get('/api/file', { parse: 'blob' });const raw = await http.get('/api/data', { parse: 'response' }); // ResponseAn empty 2xx body parses to null rather than throwing.
Errors
Section titled “Errors”The client throws HTTPError on any non-2xx response, and represents network
failures as an HTTPError with status: 0. See the
error model.
import { HTTPError } from '@calimero-network/mero-js';
try { await http.get('/api/data');} catch (err) { if (err instanceof HTTPError) { console.error(err.status, err.statusText, err.url, err.bodyText); }}Retry with backoff
Section titled “Retry with backoff”withRetry re-runs a request on transient failures — HTTP 429/5xx,
TimeoutError, and network TypeError — with exponential backoff (base 250ms,
±20% jitter) that honours Retry-After. It never retries an AbortError
(a deliberate cancellation).
import { withRetry } from '@calimero-network/mero-js';
const data = await withRetry( (attempt) => http.get('/api/flaky'), { attempts: 3 }, // default 3);createRetryableMethod(fn, opts) wraps a function so every call retries.
Cancellation & timeouts
Section titled “Cancellation & timeouts”Every request already races an internal timeout (timeoutMs). Combine it with
your own signal, or compose several:
import { combineSignals, createTimeoutSignal } from '@calimero-network/mero-js';
const user = new AbortController();const combined = combineSignals([user.signal, createTimeoutSignal(5000)]);
const data = await http.get('/api/endpoint', { signal: combined });// user.abort() cancels; a 5s timeout also cancels — whichever fires firstBoth helpers prefer the native AbortSignal.any / AbortSignal.timeout when
available and fall back gracefully.
Headers & authorization
Section titled “Headers & authorization”- Caller headers win — request-level headers override the client defaults.
- The client only sets
Authorization: Bearer <token>if the caller didn’t already provide anauthorizationheader (case-insensitive).
await http.post('/api/data', body, { headers: { 'X-Custom-Header': 'value' },});FormData
Section titled “FormData”For a FormData body the client does not set Content-Type — it lets the
browser/undici add the correct multipart boundary:
const form = new FormData();form.append('file', fileInput.files[0]);await http.post('/api/upload', form); // no content-type set by the clientNode.js and custom fetch
Section titled “Node.js and custom fetch”On Node 18+ the global fetch is used automatically. On older Node, inject one:
import { createNodeHttpClient } from '@calimero-network/mero-js';import { fetch as undiciFetch } from 'undici';
const http = createNodeHttpClient({ baseUrl: 'https://api.calimero.network', fetch: undiciFetch, getAuthToken: async () => process.env.ACCESS_TOKEN,});For a fully custom transport, hand createHttpClient a Transport:
import { createHttpClient, type Transport } from '@calimero-network/mero-js';
const transport: Transport = { fetch: customFetch, baseUrl: 'https://api.example.com', getAuthToken: async () => 'your-token', timeoutMs: 5000,};const http = createHttpClient(transport);Credentials
Section titled “Credentials”There’s no implicit same-origin default — credentials are sent only when you
ask:
const http = createBrowserHttpClient({ baseUrl: 'https://api.example.com', credentials: 'include', // send cookies on cross-origin requests});