@molecule/app-http-axios
v1.1.0
Published
Axios HTTP client provider for @molecule/app-http
Readme
@molecule/app-http-axios
Axios HTTP client provider for @molecule/app-http.
This package provides an Axios-based implementation of the molecule HttpClient interface, allowing you to use molecule's HTTP abstractions with Axios.
The body is the SECOND argument: post(url, data, config?) — likewise put and patch.
data IS the request body, so post(url, { data: body }) sends the JSON body
{"data":body}, not body, and the API finds none of its fields at the top level (it
answers 422 {"errors":{"username":["can't be blank"],…}}). Write post('/users', { user }),
never post('/users', { data: { user } }); a config (headers, params) is the THIRD argument.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { createAxiosClient } from '@molecule/app-http-axios'
import { setClient } from '@molecule/app-http'
const client = createAxiosClient({
baseURL: 'https://api.example.com',
timeout: 10000,
})
setClient(client)
// Now use via `@molecule/app-http`
import { get, post } from '@molecule/app-http'
const users = await get('/users')
const newUser = await post('/users', { name: 'John' })Type
provider
Installation
npm install @molecule/app-http-axios @molecule/app-http axiosAPI
Interfaces
AxiosHttpClientConfig
Axios-specific configuration.
interface AxiosHttpClientConfig {
/**
* Base URL for all requests.
*/
baseURL?: string
/**
* Default headers for all requests.
*/
defaultHeaders?: Record<string, string>
/**
* Default timeout in milliseconds.
*/
timeout?: number
/**
* Whether to include credentials by default.
*/
withCredentials?: boolean
/**
* The scheme of the `Authorization` header the auth token is sent with:
* `Bearer` (default), or what your API expects, e.g. `Token` for an API that
* reads `Authorization: Token <jwt>`. Pass it to this config, at construction —
* `setClient(createAxiosClient({ baseURL, authScheme: 'Token' }))` — never as a
* property of the client it returns. Same option as `@molecule/app-http`'s
* `HttpClientConfig.authScheme`, so the config reads the same whichever
* transport is bonded.
*/
authScheme?: string
/**
* Existing Axios instance to wrap (optional).
*/
instance?: AxiosInstance
/**
* Additional Axios-specific config.
*/
axiosConfig?: AxiosRequestConfig
/**
* Default retry policy for every request this client makes, or `false` to
* disable retries. A request's own `retry` overrides it. The policy is the
* same one `@molecule/app-http`'s fetch client uses: idempotent requests
* only, on 502/503/504 or a reset/refused connection, with backoff and
* jitter, honoring `Retry-After`.
*/
retry?: RetryOptions | false
}HttpClient
HTTP client interface.
All HTTP providers must implement this interface.
interface HttpClient {
/**
* Base URL for all requests.
*/
baseURL: string
/**
* Default headers for all requests.
*/
defaultHeaders: Record<string, string>
/**
* Makes a generic HTTP request. `data` is a field of THIS config object — the same body
* `post(url, data, config)` takes as its second argument.
*/
request<T = unknown>(config: FullRequestConfig): Promise<HttpResponse<T>>
/**
* Makes a GET request.
*/
get<T = unknown>(url: string, config?: RequestConfig): Promise<HttpResponse<T>>
/**
* Makes a POST request. `data` is the request BODY, not a config object: the wire body is
* `data` itself, so `post(url, { data: body })` sends `{"data":body}` — pass `body` directly.
* A config (headers, params) is the THIRD argument.
*/
post<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>
/**
* Makes a PUT request. `data` is the request BODY (second argument) — never `{ data: body }`,
* which sends `{"data":body}`; a config is the THIRD argument.
*/
put<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>
/**
* Makes a PATCH request. `data` is the request BODY (second argument) — never `{ data: body }`,
* which sends `{"data":body}`; a config is the THIRD argument.
*/
patch<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>
/**
* Makes a DELETE request.
*/
delete<T = unknown>(url: string, config?: RequestConfig): Promise<HttpResponse<T>>
/**
* Adds a request interceptor.
* Returns a function to remove the interceptor.
*/
addRequestInterceptor(interceptor: RequestInterceptor): () => void
/**
* Adds a response interceptor.
* Returns a function to remove the interceptor.
*/
addResponseInterceptor(interceptor: ResponseInterceptor): () => void
/**
* Adds an error interceptor.
* Returns a function to remove the interceptor.
*/
addErrorInterceptor(interceptor: ErrorInterceptor): () => void
/**
* Sets the authorization token.
*/
setAuthToken(token: string | null): void
/**
* Returns the current authorization token, or `null` if not set.
*/
getAuthToken(): string | null
/**
* Registers a handler for authentication errors (401).
*
* @returns An unsubscribe function.
*/
onAuthError(handler: () => void): () => void
}HttpClientConfig
HTTP client configuration.
interface HttpClientConfig {
/**
* Base URL for all requests.
*/
baseURL?: string
/**
* Default headers for all requests.
*/
defaultHeaders?: Record<string, string>
/**
* Default timeout in milliseconds.
*/
timeout?: number
/**
* Whether to include credentials by default.
*/
withCredentials?: boolean
/**
* The scheme of the `Authorization` header the auth token is sent with:
* `Bearer` (default), or what your API expects, e.g. `Token` for an API that
* reads `Authorization: Token <jwt>`. It is an option of the client's
* CONSTRUCTOR (`createFetchClient({ baseURL, authScheme: 'Token' })`, or the
* bonded provider's `createAxiosClient({ … })`), never a property of the client
* it returns.
*/
authScheme?: string
/**
* Default retry policy for every request this client makes, or `false` to
* disable retries. A request's own `retry` overrides it.
*/
retry?: RetryOptions | false
}RequestConfig
HTTP request options (headers, query params, timeout, credentials, response type, abort signal).
interface RequestConfig {
/**
* Request headers.
*/
headers?: Record<string, string>
/**
* Query parameters.
*/
params?: Record<string, string | number | boolean | undefined>
/**
* Request timeout in milliseconds.
*/
timeout?: number
/**
* Whether to include credentials (cookies).
*/
withCredentials?: boolean
/**
* Send this request WITHOUT waiting for the session restore (the auth gate).
*
* A request the gate's restore issues from a LATER continuation must set this. The gate
* recognizes a request the settle function issues while it runs and never holds it — this
* package's client awaits the gate before anything else, so a `restoreSession()` that calls
* it directly does not deadlock. But a restore that awaits something else first
* (`await storage.get()`), then calls the client, reaches `awaitAuthGate` after that window:
* the gate then waits on itself, and every request on the page stalls until the gate times
* out (3 s) — so pass `skipAuthGate: true` on that request. A provider that awaits the gate
* inside a scheduled interceptor (the Axios bond) also reaches it after the window, so its
* restore's request needs the opt-out.
*
* ```typescript
* // the request the gate's restore makes, through the same client the gate holds back
* await http.get(currentUserEndpoint, { skipAuthGate: true })
* ```
*
* `@molecule/app-auth`'s own restore does not need it — it sends its request with
* `fetch`, not through this client. An app with its own session store that registers a
* gate with `setAuthGate` does not need it either when its restore calls the client
* directly.
*/
skipAuthGate?: boolean
/**
* Response type.
*/
responseType?: 'json' | 'text' | 'blob' | 'arraybuffer'
/**
* Abort signal for cancellation.
*/
signal?: AbortSignal
/**
* Request body data, as a field of THIS config object — the `request({ method, url, data })`
* form. The verb methods take the body as their SECOND argument: `post(url, data, config)`,
* `put(url, data, config)`, `patch(url, data, config)`. Never wrap it there as
* `{ data: body }`: that object is the body, so the wire sends `{"data":body}` and the API
* sees none of the fields at its top level.
*/
data?: unknown
/**
* Custom request options (implementation-specific).
*/
options?: Record<string, unknown>
/**
* Retry policy for this request, or `false` to disable retries.
*
* The default policy retries only IDEMPOTENT requests (`GET`/`HEAD`/`PUT`/
* `DELETE`/`OPTIONS`, and `POST`/`PATCH` carrying an `Idempotency-Key`) on
* `502`/`503`/`504` or a reset/refused connection, twice, with exponential
* backoff and jitter, honoring `Retry-After`. See `@molecule/app-http`'s
* `shouldRetryRequest`.
*/
retry?: RetryOptions | false
}FullRequestConfig
Full request configuration including method and URL.
interface FullRequestConfig extends RequestConfig {
/**
* HTTP method.
*/
method: HttpMethod
/**
* Request URL (can be relative or absolute).
*/
url: string
/**
* Request body data — a field of this config object, never a second argument here.
*/
data?: unknown
}HttpResponse
Parsed HTTP response with status code, headers, and typed body data.
interface HttpResponse<T = unknown> {
/**
* Response data.
*/
data: T
/**
* HTTP status code.
*/
status: number
/**
* HTTP status text.
*/
statusText: string
/**
* Response headers.
*/
headers: Record<string, string>
/**
* Original request config.
*/
config: FullRequestConfig
}Types
RequestInterceptor
Intercepts outgoing requests to modify headers, URL, or body before the request is sent.
type RequestInterceptor = (
config: FullRequestConfig,
) => FullRequestConfig | Promise<FullRequestConfig>config— The full request configuration to transform.
ResponseInterceptor
Intercepts incoming responses to transform data, check status, or perform side effects before the response reaches the caller.
type ResponseInterceptor<T = unknown> = (
response: HttpResponse<T>,
) => HttpResponse<T> | Promise<HttpResponse<T>>ErrorInterceptor
Intercepts HTTP errors to transform, retry, or rethrow them. Throwing from this interceptor propagates the error to the caller.
type ErrorInterceptor = (error: HttpError) => HttpError | Promise<HttpError> | neverHttpMethod
HTTP request method.
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'Classes
HttpError
HTTP error with response details.
status is an HTTP status when the server answered, and 0 when it did not.
A 0 means NO response exists: the request was aborted (an AbortController, a
component's effect cleanup, a route change tearing the request down) or the
network failed before anything came back. It is not an HTTP status at all, so it
says nothing about the session — an app must never treat status < 500 as
"signed out", because every aborted request on a route change would clear the
session. The correct check is {@link isAuthFailure} (status === 401 || status === 403);
isNoResponse (status === 0) identifies this case.
declare class HttpError extends Error {
status: number
response?: HttpResponse | undefined
config?: FullRequestConfig | undefined
errorKey?: string | undefined
aborted: boolean
/**
* Builds a failed-request error.
*
* @param message - Human-readable message (already translated where a key exists).
* @param status - HTTP status, or `0` when no response was received (aborted / network failure).
* @param response - Parsed response when the server answered, otherwise undefined.
* @param config - The request config that failed.
* @param errorKey - Translation key for the failure (`http.error.aborted` for an
* aborted request, `http.error.networkError` for a real network failure).
* @param aborted - Whether the request was aborted before a response: an
* `AbortController` signal (component cleanup, a cancelled request) or the
* browser tearing the fetch down during a navigation. Distinguishes an abort
* from a network outage on the same `status === 0`, without parsing the message.
*/
constructor(
message: string,
status: number,
response?: HttpResponse | undefined,
config?: FullRequestConfig | undefined,
errorKey?: string | undefined,
aborted?: boolean,
)
}Functions
createAxiosClient(config)
Creates an Axios-based HTTP client implementing the molecule HttpClient interface.
Supports request/response/error interceptors, auth token management, and 401 error handlers.
function createAxiosClient(config?: AxiosHttpClientConfig): HttpClientconfig— Axios client configuration (base URL, headers, timeout, credentials, auth scheme, or an existing Axios instance).
Returns: An HttpClient backed by Axios with interceptor and auth token support.
Constants
provider
Default Axios-based HTTP client created with default options (no base URL, 30s timeout).
const provider: HttpClientCore Interface
Implements @molecule/app-http interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setClient } from '@molecule/app-http'
import { provider } from '@molecule/app-http-axios'
export function setupHttpAxios(): void {
setClient(provider)
}Injection Notes
Requirements
Peer dependencies:
@molecule/app-http^1.0.1axios^1.6.0
Runtime Dependencies
@molecule/app-httpaxiosCall
setClient(...)at startup, BEFORE anyget/postfrom@molecule/app-httpruns. The core'sgetClient()lazily bonds its own fetch-based fallback on first use — requests issued beforesetClient()silently bypass the Axios client (no interceptors, no auth token).get()/post()/… resolve toHttpResponse<T>— read.datafor the body (const { data: users } = await get('/users')).The exported
providerconstant is a pre-built default client (no baseURL, 30 s timeout); prefercreateAxiosClient({...})so baseURL/credentials are explicit.A bonded
baseURLapplies to every relative path, and a path that already starts with it is not prefixed twice: withbaseURL: '/api', bothget('/articles')andget('/api/articles')request/api/articles. Leave the base URL off and both ask the PAGE origin, which answers its ownindex.htmlwith a 200.An API whose scheme is not
Bearer(Authorization: Token <jwt>) passesauthScheme: 'Token'to THIS package's constructor —setClient(createAxiosClient({ baseURL, authScheme: 'Token' }))— and the auth token is then sent with that scheme. It is NOT a property of the clientsetClient()/getClient()hands around: that is a builtHttpClient, andclient.authScheme = 'Token'is a type error that changes no header. The option name is the same one@molecule/app-http'sHttpClientConfigdeclares, so the config reads the same whichever transport is bonded.instance/axiosConfigaccept raw Axios options — anything the app sets there couples it to Axios; keep app code on the@molecule/app-httpinterface so the bond stays swappable.
