@envoijs/http
v0.2.0
Published
Typed HTTP client: dispatch a request, then unwrap the envelope into T.
Maintainers
Readme
@envoijs/http
A typed HTTP client that turns transport responses and backend envelopes into a stable Promise<T>.
ESM-only. Supports Node.js 18+ and browsers with the selected adapter's required APIs.
Axios is included as a runtime dependency. Application code uses createAxiosInstance() and AxiosInstance from @envoijs/http instead of importing axios.
pnpm add @envoijs/httpQuick start
Select both transport and response policy explicitly:
import { createHttp } from "@envoijs/http";
const http = createHttp({
adapter: "fetch",
envelope: {},
defaults: {
baseURL: "/api",
timeout: 15_000,
},
hooks: {
onRequest: (ctx) => {
ctx.request.headers.Authorization = `Bearer ${token}`;
},
},
});
const user = await http.get<User>("/users/1");With envelope: {}, { code: 200, msg: "ok", data: user } resolves to user.
Response policies
// HTTP-only is the core default
createHttp({ adapter: "fetch" });
// Explicit standard { code, msg, data }
createHttp({ adapter: "fetch", envelope: {} });
// Renamed fields
createHttp({
adapter: "fetch",
envelope: {
code: "errno",
msg: "errmsg",
data: "result",
ok: (code) => code === 0,
},
});Arbitrary structures use defineEnvelope<TBody, TValue>():
const envelope = defineEnvelope<PartnerBody<User>, User>({
read: (response) => response.body as PartnerBody<User>,
kind: (body) => (body.success ? "ok" : "error"),
value: (body) => body.result,
error: (body) => new Error(body.message),
});Hooks
Hooks run in explicit phases. Client hooks precede request-local hooks.
const http = createHttp({
adapter: "fetch",
hooks: {
onRequest: [addAuthHeader, addLocaleHeader],
onRequestError: reportNetworkFailure,
onResponse: normalizeSharedResponse,
onResponseError: handleUnauthorized,
onSuccess: observeResolvedValue,
onFinally: stopTrace,
},
});
await http.get("/legacy", {
hooks: {
onResponse: normalizeOnlyThisEndpoint,
},
});Reusable middleware
const responseMiddleware = createMiddleware({
onResponseError: (ctx) => {
if (ctx.error instanceof BizError && ctx.error.kind === "unauthorized")
clearSessionAndRedirect();
},
});
const http = createHttp({
adapter: "fetch",
envelope: {
code: "status",
msg: "message",
data: "payload",
ok: (code) => code === ApiCode.Ok,
unauthorized: (code) => code === ApiCode.Unauthorized,
},
hooks: mergeMiddleware(authMiddleware, responseMiddleware),
});createMiddleware() types a reusable hook bundle. mergeMiddleware() composes bundles in declaration order. onResponseError receives the classified ctx.error, including BizError.code, kind, and source.
Adapters
createHttp({ adapter: "axios" });
createHttp({ adapter: "fetch" });
createHttp({ adapter: "ofetch" });Create a shareable axios instance without importing axios in application code:
const instance = createAxiosInstance({ withCredentials: true });
createHttp({ adapter: axiosAdapter(instance) });
createHttp({ adapter: fetchAdapter({ init: { credentials: "include" } }) });
createHttp({ adapter: ofetchAdapter({ retry: 2 }) });An existing AxiosInstance keeps its interceptors and wrappers:
function attachEnvoi(instance: AxiosInstance) {
useAxiosPlugin(instance).plugin(merge());
return createHttp({ adapter: axiosAdapter(instance) });
}The adapter guide documents plugin compatibility boundaries.
The Vue and mock guide covers vue-axios, Mokup, and axios-mock-adapter on the shared instance.
Custom transports implement { name, request } and return every HTTP response, including 4xx/5xx.
Project factories
const createProjectHttp = createHttpFactory({
adapter: "fetch",
defaults: {
baseURL: "/api",
headers: { "x-client": "seller-web" },
},
envelope: {},
hooks: mergeMiddleware(authMiddleware, errorMiddleware),
});
const http = createProjectHttp();
const reportHttp = createProjectHttp({
defaults: { baseURL: "/reports" },
hooks: { onFinally: stopReportTrace },
});Overrides replace adapter/envelope, merge defaults and headers, and append hooks.
Query libraries
Passing axios.get() directly to a query library caches AxiosResponse<Envelope<T>>; HTTP 200 business failures also resolve as success. http.get<T>() resolves T and rejects failed business codes.
const getCurrentUser = (): Promise<User> => http.get<User>("/users/me");
const { data: user } = useQuery({
key: ["current-user"],
query: getCurrentUser,
});Ability-style authorization
Fetch the auth profile through envoi, then update the consumer-owned ability instance:
const profile = await http.get<AuthProfile>("/auth/profile");
ability.update(profile);Call ability.reset() on logout or globally observed 401. Backend authorization remains mandatory.
Raw responses and errors
const response = await http.raw<Blob>("/reports/export", {
responseType: "blob",
});raw() and blob responses still enforce HTTP failures. Use ignoreResponseError: true only when inspecting a non-ok response intentionally.
Full documentation: daguanren21.github.io/envoi.
