Plugins
Extend @mrzr/api-client with plugins. Call several APIs on one session with the services plugin, or write your own with four small hooks.
Plugins are optional. Each one is a separate import, so you only ship the ones you use.
services: several APIs, one login
import { createClient } from "@mrzr/api-client";
import { services } from "@mrzr/api-client/services";
const api = createClient({
baseUrl: "https://api.example.com",
plugins: [
services({
files: "https://files.example.com",
search: { baseUrl: "https://search.example.com", timeout: 5_000 },
maps: { baseUrl: "https://maps.thirdparty.com", auth: false },
}),
],
});
await api.service("files").get("/uploads");
api.service("fiels"); // type error: not a declared serviceEvery service shares the client's session, refresh and cancellation. A service gets the token unless you set auth: false, which you should for third-party APIs.
| Option | Default | Meaning |
|---|---|---|
baseUrl | required | Where the service lives |
auth | true | Send the token and refresh on 401 |
timeout | client's | Default timeout for this service |
headers | – | Added to every call, under the call's own headers |
Services with auth are added to authOrigins for you. A call's own options still win: api.service("files").get("/x", { timeout: 60_000 }). A service whose baseUrl is empty, usually an unset env variable, fails at setup with an error naming the service.
If two APIs need different logins, create two clients with different storageKeys instead.
Writing a plugin
A plugin is an object with a name and any of four hooks:
import type { ApiPlugin } from "@mrzr/api-client";
const traceIds: ApiPlugin = {
name: "trace-ids",
beforeRequest: (request) => ({
...request,
config: {
...request.config,
headers: { ...request.config?.headers, "X-Trace-Id": crypto.randomUUID() },
},
}),
};
createClient({ baseUrl, plugins: [traceIds] });| Hook | Runs | Use it to |
|---|---|---|
configure(options) | Once, before the client is created | Change client options |
beforeRequest(request) | Before each get/post/put/patch/delete | Rewrite the method, URL, body or config |
afterResponse(result, request) | After each of those calls, before throwError applies | Reshape results, collect metrics, turn a failure into a fallback |
extend(client) | Once, after the client is created | Add methods, like api.service() |
Plugins run in the order you list them. login, logout and refresh don't pass through beforeRequest or afterResponse.
Examples
// configure: defaults every request should have
const tenant: ApiPlugin = {
name: "tenant",
configure: (options) => ({ ...options, headers: { ...options.headers, "X-Tenant": "acme" } }),
};
// afterResponse: count responses per endpoint
const metrics: ApiPlugin = {
name: "metrics",
afterResponse: (result, request) => {
stats.record(request.method, request.url, result.statusCode);
return result;
},
};
// afterResponse: a fallback instead of an error
const flagsFallback: ApiPlugin = {
name: "flags-fallback",
afterResponse: (result, request) =>
!result.status && request.url === "/flags" ? { ...result, status: true, data: {} } : result,
};afterResponse runs before throwError, so a result it marks as successful doesn't throw.
Adding typed methods
Give ApiPlugin the shape of what extend returns, and the client gets those methods, fully typed:
const health: ApiPlugin<{ health(): Promise<boolean> }> = {
name: "health",
extend: (client) => ({
health: async () => (await client.get("/health", { throwError: false })).status,
}),
};
const api = createClient({ baseUrl, plugins: [health] });
await api.health(); // typedRules
- A failing plugin fails one call, never the client. A hook that throws resolves that call as a failure whose message names the plugin, and the next call runs normally. A plugin that throws in
configuremakescreateClientthrow. - Plugins can't replace built-in methods. An
extendthat returnsget,loginor another plugin's method throws at setup. - Plugins never see the token. They run on the page, around the request. They can still see every request and response, and
configurecan add trusted origins, so install plugins you trust like any dependency.
WebSockets & Socket.io
Authenticate WebSocket and socket.io connections with @mrzr/api-client using a short-lived socket ticket, without exposing the access token to the page.
Frameworks
Use @mrzr/api-client with TanStack Query, SWR, React, Next.js App Router and Vue. Short, copy-paste setups for each, including server rendering.