Client library
@thuum/client
The TypeScript client for browsers, Node.js, Deno and Bun. Tokens, domain clients, REST helpers, sockets and typed errors.
@thuum/client is a small, dependency-free TypeScript client for the whole API. It only needs
fetch (and WebSocket for sockets), so it runs in browsers, Node.js 22+, Deno and Bun. Every
REST route has a typed method; both WebSockets have a socket class with promise-based operations.
Create a client#
import { ThuumClient } from '@thuum/client';
const thuum = new ThuumClient({ baseUrl: 'https://api.thuum.dev' });| Option | Meaning |
|---|---|
baseUrl | The API's base URL. A trailing / is ignored. |
fetch | A custom fetch (e.g. a framework's request-scoped one). Defaults to the global. |
Tokens and domain clients#
Exchange a key for a token, then create a domain client bound to that token:
const { token, expires_in } = await thuum.exchangeToken('acme', process.env.THUUM_APP_KEY!);
const acme = thuum.domain('acme', token, {
onError(error) {
if (isApiError(error, 401)) {
// The token expired or the key was deleted: get a new token, sign the user out, …
}
}
});
const me = await acme.me();A domain client is cheap: create one per token. onError sees every error before it is thrown, which
is a good place to handle 401s once for the whole app.
In a browser, you usually don't exchange a key at all: your server hands the browser a token, and the
browser opens a socket with it. You don't even need a ThuumClient there:
import { EventBrokerSocket } from '@thuum/client';
const { token } = await fetch('/api/thuum-token').then((r) => r.json());
const socket = new EventBrokerSocket({ baseUrl: 'https://api.thuum.dev', domain: 'acme', token });What a domain client offers#
| Group | Methods | Needs |
|---|---|---|
me() | — | any token |
eventBroker.data | list, publish, purge | Event Broker data R, P, W |
eventBroker.topics | list, get, put, delete | Event Broker topic flags R / W |
eventBroker.connect() | returns an EventBrokerSocket | an Event Broker claim |
stateBroker.data | list, get, set, merge, delete, echo | State Broker data R, P |
stateBroker.topics | list, get, put, delete | State Broker topic flags R / W |
stateBroker.connect() | returns a StateBrokerSocket | a State Broker claim |
domain.keys | list, get, create, delete | Domain Claim domain R / W |
domain.claims | list, get, create, update, delete, protocols.put/delete, keys.link/unlink | Domain Claim domain R / W |
eventBroker.claims, .keys | claims CRUD, topics.put/delete, keys.link/unlink; key picker | Domain Claim event R / W |
stateBroker.claims, .keys | same, for State Broker claims | Domain Claim state R / W |
Every method returns the response's data, typed. Full signatures are in the
reference.
const event = await acme.eventBroker.data.publish('orders:1042', {
type: 'order.shipped',
data: { carrier: 'DHL' }
});
const history = await acme.eventBroker.data.list('orders', {
since: new Date(Date.now() - 3_600_000)
});
const state = await acme.stateBroker.data.merge('orders:1042', 'status', { stage: 'shipped' });
const states = await acme.stateBroker.data.list('orders', { prefix: 'status', limit: 1000 });Time filters accept epoch milliseconds or Date objects.
Errors#
Every error extends ThuumError:
| Class | When | Useful fields |
|---|---|---|
ThuumApiError | The API answered with a non-2xx status. | status, message (the API's detail), errors, fieldErrors |
ThuumConnectionError | No response at all (DNS, refused, aborted). | url, cause |
EventBrokerSocketError, StateBrokerSocketError | A socket op failed, or the socket closed first. | code |
import { isApiError, ThuumConnectionError } from '@thuum/client';
try {
await acme.stateBroker.data.get('orders:1042', 'status');
} catch (e) {
if (isApiError(e, 404)) return null; // no such state
if (isApiError(e, 422)) console.error(e.fieldErrors); // { topic: ['must be …'] }
if (e instanceof ThuumConnectionError) retryLater();
throw e;
}Node.js#
Node.js 22 and later have a global WebSocket, so sockets work as in the browser. Elsewhere, pass
any standard-compatible implementation, such as the ws package:
import WebSocket from 'ws';
const socket = acme.eventBroker.connect({
WebSocket: WebSocket as unknown as typeof globalThis.WebSocket
});Keep App Keys in environment variables or a secret manager, exchange them on the server only, and see Backend integration for long-running workers.
Low-level requests#
thuum.request(method, path, { token, body }) calls any route and returns its data, with the same
error handling. Useful for routes added after your client version:
const data = await thuum.request('GET', '/api/acme/me', { token });