# @mongez/http — Full Reference > **Auto-trigger when loading this full reference:** `Http`, `HttpError`, `Resource`, `CancellablePromise`, `CancellableAsyncIterable`, `ResourceService`, `CacheDriver`, `HttpConfig`, `RequestOptions`, `StreamRequestOptions`, `BeforeInterceptor`, `AfterInterceptor`, `AfterInterceptorContext`, `HttpRetryConfig`, `HttpCacheConfig`, `setCurrentHttp`, `getCurrentHttp`, `makeCancellable`, `http.get`, `http.post`, `http.stream`, `http.all`, `http.race`, `http.before`, `http.after`, `http.invalidate` imported from `@mongez/http`; user asks to make HTTP requests, handle errors, stream SSE/NDJSON, cache responses, write RESTful CRUD, intercept or retry requests, download files with progress, or use the client on a **server** (Next.js server components / SSR / Node / workers — `dedupe`, `dedupeKey`, `fetchInit`, `next: { revalidate, tags }`, `revalidateTag`). > > **Skip when:** using `axios`, `ky`, `ofetch`, or native `fetch` directly without `@mongez/http`; framework-specific data fetchers like `swr` or `@tanstack/react-query` in isolation (unless wired to `@mongez/http`); Node.js `http`/`https` core modules. --- ## Overview `@mongez/http` v3 is a zero-Axios, native-`fetch` HTTP client for TypeScript applications. It replaces the old Axios-based `Endpoint` + `RestfulEndpoint` with a cleaner architecture: - **`Http` class** — sends requests, manages config, interceptors, events - **`Resource` class** — RESTful CRUD helper built on top of Http - **`{data, error}` pattern** — every method returns a discriminated union; no try/catch noise - **`CancellablePromise`** — `.cancel()` and `.signal` on every returned promise - **`CancellableAsyncIterable`** — `.cancel()` and `.error` on every stream - **`HttpError`** — structured error with status predicates Runtime dependency: `@mongez/concat-route` only. --- ## Http class ### Constructor ```ts new Http(config?: HttpConfig) ``` ### HttpConfig ```ts interface HttpConfig { baseURL?: string auth?: string | ((req: OutgoingRequest) => string | null | undefined) putToPost?: boolean // default false — convert PUT→POST for file uploads putMethodKey?: string // default "_method" timeout?: number // ms, no timeout by default headers?: Record params?: HttpParams // default query params merged into every request cache?: boolean | HttpCacheConfig retry?: HttpRetryConfig publishKey?: string // default "published" // Fetch-native options forwarded to every fetch() call credentials?: RequestCredentials mode?: RequestMode keepalive?: boolean redirect?: RequestRedirect fetchCache?: RequestCache // browser HTTP cache directive — distinct from app-level `cache` // Extra RequestInit keys forwarded verbatim to every fetch() — the escape hatch for // anything the built-in list doesn't model (Next.js `next: {revalidate, tags}`, Deno/ // Bun/Cloudflare flags). Merged FIRST; library-managed fields still win. See "Server-side". fetchInit?: RequestInit & Record // Custom body serializer (e.g. MessagePack/CBOR). FormData/Blob/string pass through. serializer?: (data: unknown) => { body: BodyInit; contentType: string } // GET deduplication. Default: true in a browser, false everywhere else. dedupe?: boolean // Custom GET deduplication key — default keys by URL + serialised params. // Browser-only tuning: never receives headers, so it cannot tell two users apart. dedupeKey?: (url: string, params?: HttpParams) => string } ``` ### Methods ```ts // HTTP verbs — all return CancellablePromise> get(path, options?) post(path, data?, options?) put(path, data?, options?) patch(path, options?) // body via options.data delete(path, options?) head(path, options?) options(path, options?) request(method, path, data?, options?) // escape hatch for any verb // Streaming stream(path, options?: StreamRequestOptions): CancellableAsyncIterable // Concurrent helpers — cancel() cancels every inner request all(requests: CancellablePromise[]): CancellablePromise // all settle race(requests: CancellablePromise[]): CancellablePromise // first wins; rest cancelled // Cache management invalidate(key: string): Promise invalidateAll(): Promise // requires CacheDriver.clear() // Config extend(overrides: HttpConfig): Http // new instance, merged config getConfig(): Readonly // Interceptors before(fn: BeforeInterceptor): this after(fn: AfterInterceptor): this // runs on success AND error // Events on(event: string, handler: HttpEventHandler): this off(event: string, handler: HttpEventHandler): this ``` --- ## HttpResult ```ts type HttpResult = | { data: T; error: null; status: number; response: Response; headers: Record; request: OutgoingRequest } | { data: null; error: HttpError; status: number | null; response: Response | null; headers: Record | null; request: OutgoingRequest } ``` **Always pass the response type as a generic.** Every verb is `get(…)`, so omitting `T` makes `data` `unknown` and `data.first_name` won't type-check (the #1 mistake when migrating from axios/fetch). Pass it explicitly and narrow on `error` first (the union flips `data` to non-null `T` on the success branch): ```ts const { data, error } = await http.get('/users/1'); // data: User | null if (error) return; // data is null here console.log(data.first_name); // data: User — type-checks // arrays: http.get('/users'); POST: http.post('/users', body) ``` Use the generic, not an `as`/`any` cast — it flows through `all`/`race`/`Resource` too. --- ## CancellablePromise ```ts type CancellablePromise = Promise & { cancel(reason?: string): void readonly signal: AbortSignal } ``` Factory: `makeCancellable(factory, externalSignal?)` --- ## CancellableAsyncIterable ```ts type CancellableAsyncIterable = AsyncIterable & { cancel(reason?: string): void readonly error: HttpError | null // set after iteration ends with an error } ``` Returned by `http.stream()`. Iteration ends silently on cancel; check `.error` afterwards. --- ## RequestOptions ```ts interface RequestOptions { params?: HttpParams headers?: Record signal?: AbortSignal cache?: boolean | Omit & { driver?: CacheDriver } cacheKey?: string // explicit cache key; required server-side when cache is on dedupe?: boolean // override the dedupe default for this call dedupeKey?: string // explicit identity-aware dedup key; required server-side retry?: boolean | Partial throw?: boolean // default false — throw HttpError instead of returning it timeout?: number data?: unknown // body for PATCH / DELETE and any method needing a body responseType?: 'json' | 'text' | 'blob' | 'arrayBuffer' | 'stream' onDownloadProgress?: (event: DownloadProgressEvent) => void onUploadProgress?: (event: UploadProgressEvent) => void // Per-request fetch-native overrides credentials?: RequestCredentials mode?: RequestMode keepalive?: boolean redirect?: RequestRedirect fetchCache?: RequestCache fetchInit?: RequestInit & Record // merged over HttpConfig.fetchInit } ``` `responseType: "stream"` returns raw `ReadableStream` — body is never read by the library. `onDownloadProgress` and `cache` are silently ignored when `responseType: "stream"` is set. --- ## HttpError ```ts class HttpError extends Error { status: number | null // null for network/abort/timeout body: unknown // parsed response body response: Response | null headers: Record | null request: OutgoingRequest | null // sensitive headers redacted ("[redacted]") rawRequest: OutgoingRequest | null // original, unredacted headers — never log this isAborted: boolean isTimeout: boolean isNetwork: boolean // Status predicates — getters, no () needed get isClientError(): boolean // 4xx get isServerError(): boolean // 5xx get isUnauthorized(): boolean // 401 get isForbidden(): boolean // 403 get isNotFound(): boolean // 404 get isValidationError(): boolean // 422 get isRateLimited(): boolean // 429 toJSON(): Record // omits `request` / `rawRequest` } ``` --- ## Resource class ```ts class Resource { route: string defaultListParams: HttpParams list(params?, options?): CancellablePromise> get(id, options?): CancellablePromise> create(data, options?): CancellablePromise> update(id, data, options?): CancellablePromise> patch(id, options?): CancellablePromise> // body via options.data delete(id, options?): CancellablePromise> bulkDelete(data, options?): CancellablePromise> publish(id, published, publishKey?, options?): CancellablePromise> action(id, actionName, data?, options?, method?): CancellablePromise> path(suffix?): string actionPath(id, actionName): string useHttp(instance): this } ``` `http` getter is lazy — resolved from `getCurrentHttp()` on first request. --- ## CacheDriver ```ts interface CacheDriver { get(key: string): Promise set(key: string, value: unknown, ttl?: number): Promise | void remove?(key: string): Promise | void clear?(): Promise | void // required for invalidateAll() } ``` ### HttpCacheConfig ```ts interface HttpCacheConfig { driver: CacheDriver ttl?: number // default 300s generateKey?: (url, params?) => string // default: baseURL + path + params JSON } ``` The cache is **opt-in** — with no driver configured it never runs. It is also a **client-side** feature: the default key is URL + params and cannot tell two callers apart, so outside a browser an enabled cache requires `cacheKey` or `generateKey` and throws without one. See "Server-side". --- ## Server-side (Next.js / SSR / Node) Everything below keys off `typeof window === "undefined"`. **Browser behaviour is unchanged.** ### GET deduplication Concurrent GETs to the same key share one underlying fetch. The key is URL + params — it never sees credentials — so on a server two `GET /me` calls with different bearer tokens would share one fetch and both callers would get the first caller's response. ```ts dedupe: typeof window !== 'undefined' // the default: on in a browser, off on a server ``` Opting in server-side requires a per-request identity-aware key; the library will not guess which header carries identity (`Authorization`? a cookie? `X-Tenant-Id`?): ```ts const http = new Http({ baseURL, dedupe: true }); await http.get('/me', { dedupeKey: `me:${session.userId}` }); // ✅ await http.get('/me'); // ❌ throws ``` `HttpConfig.dedupeKey(url, params)` does **not** satisfy this — it only ever receives URL + params. It stays a browser-side tuning knob. ### Response cache Same blind spot. Server-side, an enabled cache needs `cacheKey` or a `generateKey` closing over the caller, else the request throws. Prefer the framework's cache instead. ### fetchInit — Next.js `next: { revalidate, tags }` The library builds its `RequestInit` from a fixed set of keys, so unmodelled ones were dropped. Tags attach per-fetch, so without forwarding, `revalidateTag()` invalidates nothing — correct-looking in dev, broken in production. ```ts const http = new Http({ baseURL, fetchInit: { next: { revalidate: 60 } } }); await http.get('/posts', { fetchInit: { next: { tags: ['posts'] } } }); import { revalidateTag } from 'next/cache'; revalidateTag('posts'); // now actually invalidates ``` Merged **first**, so library-managed fields win: `method` / `headers` / `body` / `signal` can never be overridden; `credentials` / `mode` / `keepalive` / `redirect` / `cache` win only when actually configured, so an unset option won't erase a `fetchInit` value. Applied at **both** fetch call sites — regular requests and `stream()` (except `keepalive`, never applied to a stream). Use the first-class `fetchCache` option for plain `no-store` / `force-cache`. ### setCurrentHttp is global state Module-level singleton — safe in a browser, shared across concurrent requests on a server. Don't call it per request to attach a user's token; build a request-scoped `new Http({ auth })` and pass it explicitly (`new OrdersResource().useHttp(http)`). --- ## HttpRetryConfig ```ts interface HttpRetryConfig { attempts: number delay: number // base ms backoff?: boolean // default true — delay * 2^attempt jitter?: boolean // default false — multiply delay by random [0.5, 1.0) retryOn?: number[] // default [429, 500, 502, 503, 504] onRetry?: (attempt: number, error: HttpError, delay: number) => void } ``` Network errors are always retried. Aborts and timeouts are never retried. --- ## Interceptors ```ts // Before: modify outgoing request (receives read-only RequestOptions snapshot as 2nd arg) type BeforeInterceptor = ( req: OutgoingRequest, options: Readonly, ) => OutgoingRequest | void | Promise // After: transform result (success OR error); context.replay() re-fires original request interface AfterInterceptorContext { replay(): Promise> } type AfterInterceptor = ( result: HttpResult, context: AfterInterceptorContext, ) => HttpResult | void | Promise | void> ``` --- ## Streaming ```ts interface StreamRequestOptions { method?: HttpMethod // default "GET" data?: HttpData format?: 'sse' | 'ndjson' // default "sse" parseLine?: (line: string) => unknown params?: HttpParams headers?: Record signal?: AbortSignal timeout?: number reconnect?: boolean // default false maxReconnectAttempts?: number // default Infinity reconnectDelay?: number // default 3000 ms } ``` ```ts for await (const chunk of http.stream('/chat', { method: 'POST', data: body })) { process(chunk); } // stream.error is set if the stream ended with an error ``` --- ## Bootstrap ```ts import { Http, setCurrentHttp } from '@mongez/http'; const http = new Http({ baseURL: import.meta.env.VITE_API_URL, auth: () => localStorage.getItem('token') ? `Bearer ${localStorage.getItem('token')}` : null, retry: { attempts: 2, delay: 300 }, }); setCurrentHttp(http); export { http }; ``` --- ## File map | File | Exports | |------|---------| | `src/Http.ts` | `Http` | | `src/HttpError.ts` | `HttpError` | | `src/Resource.ts` | `Resource` | | `src/Resource.types.ts` | `ResourceService` | | `src/cancellable.ts` | `CancellablePromise`, `CancellableAsyncIterable`, `makeCancellable` | | `src/current-http.ts` | `setCurrentHttp`, `getCurrentHttp` | | `src/Http.types.ts` | All shared types/interfaces | | `src/utils/params.ts` | `buildQueryString`, `appendParams` | | `src/utils/body.ts` | `prepareBody`, `parseBody` | | `src/index.ts` | Re-exports everything | --- ## Changelog ### 3.4.0 — 2026-08-08 (server-side safety; browser behaviour unchanged) - **Fixed:** GET deduplication could serve one user's response to another on a server — the key never included credentials. Deduplication now defaults to **off** outside a browser. - `HttpConfig.fetchInit` / `RequestOptions.fetchInit` — extra `RequestInit` keys forwarded verbatim to fetch (Next.js `next: { revalidate, tags }`, runtime flags). Applied at both fetch call sites, including `stream()`. Cannot override `method`/`headers`/`body`/`signal`. - `HttpConfig.dedupe` / `RequestOptions.dedupe` — toggle GET deduplication. - `RequestOptions.dedupeKey` — explicit identity-aware dedup key; required server-side. - **Changed:** an enabled response cache now requires `cacheKey` or `cache.generateKey` outside a browser, and throws without one. ### 3.3.x - `responseType: "stream"` — raw `ReadableStream` pass-through - `http.all()` / `http.race()` — concurrent request helpers with cancel-all semantics - `HttpConfig.fetchCache` / `RequestOptions.fetchCache` — fetch-native `RequestCache` directive - `HttpConfig.dedupeKey` — custom GET deduplication key generator - `HttpConfig.serializer` — custom body serializer (MessagePack, CBOR, etc.) - `BeforeInterceptor` second param: `Readonly` — inspect per-request options in before-interceptors ### 3.0.0 - Complete rewrite: native `fetch`, drop Axios - `{data, error}` result pattern - `CancellablePromise` on all methods - `HttpError` with typed predicates - `Resource` replaces `RestfulEndpoint` (fixes double-URL bug in `publish`) - Lazy `http` getter in Resource (fixes silent undefined) - Built-in caching (any CacheDriver) - Built-in retry with exponential backoff - Before/after interceptors - `Http.extend()` for scoped instances - `putToPost` for Laravel-style file uploads