Step 3: Call it — and handle it when it fails
useApi turns your endpoint into a typed function call — no fetch, no JSON — and a real error when it fails.
Your component calls its own endpoint as a typed function, not a URL:
import { type ToolRenderProps, WorkshopApiError, defineTool, useApi } from "@vvd/sdk"
interface OracleApi {
rollOracle(): Promise<{ roll: string }>
}
function OracleRoll({ context }: ToolRenderProps) {
const api = useApi<OracleApi>()
// ...
const rollOracle = async () => {
setStatus("loading")
try {
const { roll } = await api.rollOracle()
setRoll(roll)
setStatus("idle")
} catch (err) {
// A throw inside the handler comes back SANITIZED — the real error goes to
// the server log, never the browser. err.message is always safe to show.
setStatus("error")
console.error(err instanceof WorkshopApiError ? err.message : err)
}
}
// ...
}rollOracle → api/roll-oracle.ts — camelCase in your call, kebab-case on disk. No fetch,
no JSON, and no tool id: the host binds this mount's tool id into useApi, so your code
physically cannot reach a different tool's endpoints through this door.
This page can't run that
There's no api/ folder behind this browser tab — no server to dispatch to, so useApi has
nothing to call here. The playground below wires up a stand-in with the exact same shape —
the same await, the same loading state, the same failure — so the code you write is the
code that ships. Swap in useApi for real once you run this locally (Step 5).
Your turn. The button below does nothing yet. Give it a roll, a loading state while it waits, and a message instead of a crash when it fails:
Edit and the example re-runs. Tab indents; press Escape to leave the editor.
You should see: press Ask the oracle — it says "Consulting…" for half a second, then a real answer. Press Break it, on purpose and you get a message instead of a blank screen or a crash.