Skip to main content

Client chaining

client.with({ ... }) returns a client that adds the same data to every request, like an authorization header.

const client = createClient<typeof routes>({
path: "http://localhost:8080/rpc",
});

const authedClient = client.with({
middleware: () => ({
headers: { authorization: localStorage.getItem("token") || "nothing" },
}),
});

// GOOD:
await fetchCuple(authedClient.getProfile.get);

// ALSO GOOD:
await fetchCuple(client.getProfile.get, {
headers: { authorization: localStorage.getItem("token") || "nothing" },
});

// A route that requires the header, called without it, is a type error:
await fetchCuple(client.getProfile.get); // TYPE ERROR

Options​

OptionRunsFor
middlewareBefore every request; awaited, so it can refresh a token.Data every request needs: headers, a tenant id.
keyWhen a cache needs it (a string, or a synchronous getter).Telling caches whose data a response is. See Switching accounts.
finalwareAfter every response, before it's handed back.Cross-cutting reactions: redirect on an expired session, report failures. Throwing turns a result into an exception.
  • NOTE: key is what the response depends on (who the user is), not the injected bytes (the token). A refreshed token is the same user.
  • NOTE: finalware can replace the response, but the types don't follow it. Observe or throw; don't reshape.
Why it works this way

This replaces HTTP interceptors (as in axios). An interceptor adds the auth header out of sight of the types, so a request that misses it is a 401 found at runtime. Here the route's schema makes the header part of the call's type: a call without it doesn't compile, and with() gives you a client whose type says the header is already supplied.

with() returns a new client instead of changing the one you have, so one base client can have variants side by side: public, signed in, admin.

middleware runs per request because tokens expire. Reading the token when the request is sent means a long-lived client never sends a stale one.

key is separate from middleware because a cache needs it synchronously, and middleware is async. It also names who the data belongs to, not the token: a refreshed token is the same user.

finalware can replace the response, but the types don't follow. Fixing that would be welcome, but it isn't critical: nothing relies on it.