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
| Option | Runs | For |
|---|---|---|
middleware | Before every request; awaited, so it can refresh a token. | Data every request needs: headers, a tenant id. |
key | When a cache needs it (a string, or a synchronous getter). | Telling caches whose data a response is. See Switching accounts. |
finalware | After 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:
keyis what the response depends on (who the user is), not the injected bytes (the token). A refreshed token is the same user. - NOTE:
finalwarecan replace the response, but the types don't follow it. Observe or throw; don't reshape.
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.