@mrzr/api-client

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 service

Every 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.

OptionDefaultMeaning
baseUrlrequiredWhere the service lives
authtrueSend the token and refresh on 401
timeoutclient'sDefault 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] });
HookRunsUse it to
configure(options)Once, before the client is createdChange client options
beforeRequest(request)Before each get/post/put/patch/deleteRewrite the method, URL, body or config
afterResponse(result, request)After each of those calls, before throwError appliesReshape results, collect metrics, turn a failure into a fallback
extend(client)Once, after the client is createdAdd 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();   // typed

Rules

  • 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 configure makes createClient throw.
  • Plugins can't replace built-in methods. An extend that returns get, login or 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 configure can add trusted origins, so install plugins you trust like any dependency.

On this page