Revalidation & subscriptions
The client’s cache is not a passive store — it actively keeps GET data fresh. There are four mechanisms, all built on the same tag index and Observable subscription protocol.
1. Tag-based revalidation after mutations
Section titled “1. Tag-based revalidation after mutations”Toapi servers attach cache tags to GET responses via the X-TAPI-Tags header (declared server-side with cache: { tags: [...] }). The client records which URLs carry which tags in a tag index.
Every mutation response (POST/PUT/PATCH/DELETE) may also carry an X-TAPI-Tags header listing the tags it invalidated. When such a response arrives, the client looks up every cached URL that shares any of those tags, drops it from the cache, and notifies subscribers so they can re-fetch. A tagged URL with no active subscriber is simply dropped and re-fetched lazily on its next .get() call.
// This POST responds with, say, tag "users".// Any cached GET tagged "users" is revalidated automatically.await client.users.post({ name: "Alice" });Awaiting revalidation: .revalidated
Section titled “Awaiting revalidation: .revalidated”The promise returned by a mutation is augmented with a .revalidated property:
type Revalidating = { revalidated: Promise<void>;};.revalidated settles once the tag-based revalidation triggered by that mutation’s response has completed. Await it when you need dependent GETs to be up to date before proceeding:
const result = client.users.post({ name: "Alice" });await result; // the created userawait result.revalidated; // all matching GET caches are now fresh2. Imperative revalidation: .revalidate()
Section titled “2. Imperative revalidation: .revalidate()”Call .revalidate(query?) on any route to drop its cached GET entry and notify subscribers, forcing a re-fetch. It resolves once that has happened:
await client.users.revalidate();await client.users.get({ active: true }); // now served from fresh cache3. Time-based revalidation
Section titled “3. Time-based revalidation”When the server sends an X-TAPI-Expires-At header, the client schedules a background revalidation for that time (plus a small random jitter bounded by maxOverdueTTL, to avoid stampedes). This happens regardless of whether the entry currently has subscribers. maxOverdueTTL is configurable via createFetchClient options.
4. Server-pushed invalidation
Section titled “4. Server-pushed invalidation”The client can receive invalidations pushed by the server, so open views stay current even when the change originates elsewhere (another user, a background job). This happens over a long-lived connection to the invalidations route:
INVALIDATIONS_ROUTE— re-exported from@toapi/client, equal to"/__tapi/invalidations". By default the client connects toapiUrl + INVALIDATIONS_ROUTE.- The stream is newline-delimited; each line is a space-separated list of tags to revalidate.
- On (re)connect, the entire cache is invalidated and subscribers are notified, since it may have gone stale while disconnected.
- The client reconnects automatically with exponential backoff on network errors.
- In browsers with a controlling service worker, invalidations are instead delivered via
postMessage(event typeTAPI_INVALIDATE_TAGS, plusTAPI_CONNECTon (re)connect), and the direct stream is not opened.
Configure or disable this with the invalidationsUrl option:
// custom endpointcreateFetchClient<typeof api.routes>(apiUrl, { invalidationsUrl: "https://example.com/api/__tapi/invalidations",});
// disable server-push entirelycreateFetchClient<typeof api.routes>(apiUrl, { invalidationsUrl: false });Failed requests
Section titled “Failed requests”A GET that rejects (a non-2xx response, or a network error) is removed from the cache as soon as it fails, and the error is reported via options.logger.error. The next .get() call for that URL issues a fresh request rather than replaying the same rejection.
Subscriptions tie it together
Section titled “Subscriptions tie it together”All revalidation ultimately surfaces through subscribers. Awaiting a .get() gives you a one-shot value; subscribing registers a callback for every subsequent value:
const result = client.todos.get();const unsubscribe = result.subscribe((next) => { next.then((todos) => render(todos));});
// A mutation elsewhere, a server push, or a timed revalidation// will now re-run this callback with fresh data.
unsubscribe(); // stop listeningSee Observable for the subscription API in detail.