Adapters
An adapter executes the normalized request. envoi applies baseURL, serializes query parameters with ufo, merges common headers, and combines timeout with an external AbortSignal before the adapter runs.
Axios
Axios is built in but never selected implicitly:
const http = createHttp({ adapter: "axios" });An existing application should pass the instance it already owns. The reusable bridge receives that instance; it does not import or create axios:
import { axiosAdapter, createHttp, type AxiosInstance } from "@envoijs/http";
export function createProjectHttp(instance: AxiosInstance) {
return createHttp({
adapter: axiosAdapter(instance),
});
}Call createProjectHttp() with the exact object the host application already gives to its interceptors, Vue plugin, or request wrapper. The instance keeps its baseURL, credentials, and native defaults.
The value must be an actual AxiosInstance with request, defaults, and interceptors. If the existing module exports only a convenience function, export its underlying instance separately. Move configuration into createHttp.defaults only as a deliberate migration; do not configure the same field in both places.
For a new application, install only envoi and create the shareable instance through its public factory:
import { axiosAdapter, createAxiosInstance, createHttp } from "@envoijs/http";
const instance = createAxiosInstance({
baseURL: "/api",
timeout: 15_000,
withCredentials: true,
xsrfCookieName: "XSRF-TOKEN",
});
export const http = createHttp({
adapter: axiosAdapter(instance),
});@envoijs/plugins on existing Axios instances
@envoijs/plugins extends the exact Axios instance that envoi dispatches through:
pnpm add @envoijs/pluginsimport { axiosAdapter, createHttp, type AxiosInstance } from "@envoijs/http";
import { installPlugins, merge, normalize } from "@envoijs/plugins";
export function installProjectPlugins(instance: AxiosInstance) {
installPlugins(instance, [normalize(), merge()]);
return createHttp({
adapter: axiosAdapter(instance),
});
}Install plugins before the first request. installPlugins mutates the project-owned instance in place, and envoi calls instance.request(), so the installed plugin lifecycle remains active.
Per-request axios plugin fields go through the namespaced metadata escape hatch:
await http.get("/orders", {
meta: {
axios: {
merge: true,
},
},
});The axios adapter merges meta.axios into AxiosRequestConfig, then applies envoi's canonical URL, method, body, headers, signal, timeout, response type, and validateStatus. Plugin options cannot override those protocol invariants.
Compatibility boundaries
| Plugin behavior | Rule |
|---|---|
| merge, debounce, cache, loading, cancel, mock, request normalization | Safe when the plugin ultimately returns a complete AxiosResponse |
| response transform | Must return the complete AxiosResponse; returning response.data breaks the adapter contract |
| retry | Transport errors are safe. HTTP 4xx/5xx remain responses because envoi sets validateStatus: () => true |
| throttle give-up | Use the throwing mode. A silent or empty result is not an AxiosResponse |
| business-code unwrap or reject | Keep it in the envoi envelope so HTTP and business classification have one owner |
The plugin library runs request-side hooks forward and response/error/completion hooks in reverse registration order. Keep order-dependent plugins beside the axios instance. This integration boundary has no framework runtime dependency.
Native fetch
import { createHttp, fetchAdapter } from "@envoijs/http";
const http = createHttp({
adapter: fetchAdapter({
init: {
credentials: "include",
cache: "no-store",
},
}),
});A custom fetch implementation can be injected for a runtime bridge or test.
ofetch
Install the optional peer before selecting the adapter:
pnpm add ofetchimport { createHttp, ofetchAdapter } from "@envoijs/http";
const http = createHttp({
adapter: ofetchAdapter({
retry: 2,
retryDelay: 250,
}),
});Custom transport
import type { Adapter } from "@envoijs/http";
const nativeAdapter: Adapter = {
name: "native-bridge",
async request(request) {
const response = await nativeBridge.request(request);
return {
status: response.status,
statusText: response.statusText,
headers: response.headers,
body: response.body,
raw: response,
};
},
};Contract
Adapters receive a final URL and parsed request options. They must return all HTTP responses, including 4xx and 5xx. They throw only transport failures such as a network error, abort, or timeout.
A built-in adapter is accepted only after it passes the shared conformance suite for:
- baseURL and absolute URLs;
- arrays and nested query values;
- JSON and native request bodies;
- responseType;
- timeout plus external signal;
- success and HTTP failure responses.