External API client
ExternalApiClient is the Workspace UI Project’s client for third-party HTTP
APIs — exchange rates, geocoding, public product data, or any other service a
page calls directly from the browser. It exists alongside the
Moltaro API client
so the two traffic kinds stay separated: workspace data goes through
MoltaroApiClient, everything else goes through ExternalApiClient.
import { ExternalApiClient, ExternalApiClientError } from '@moltaro/workspace-ui'
const api = new ExternalApiClient()The credential boundary
Section titled “The credential boundary”ExternalApiClient never attaches the Moltaro bearer token or any workspace
header to a request. It also defaults credentials to omit, so browser
cookies are not sent either unless page code explicitly opts in via
RequestInit.credentials. This is the entire point of using the client
instead of raw fetch: a typo’d or attacker-controlled destination URL cannot
receive the caller’s Moltaro session.
Workspace UI code is trusted workspace code compiled by Moltaro — the boundary here is governance and review, not a hostile-code sandbox. The client makes the safe behavior the default; it does not prevent a reviewer- approved page from deliberately sending credentials somewhere.
Methods
Section titled “Methods”| Method | Returns | Non-2xx behavior |
|---|---|---|
request(input, options?) | raw Response | resolves; check response.ok yourself |
requestJson<T>(input, options?) | parsed JSON as T | throws ExternalApiClientError |
requestText(input, options?) | string | throws ExternalApiClientError |
requestBlob(input, options?) | Blob | throws ExternalApiClientError |
ExternalApiClientError carries the response status and statusText:
try { return await api.requestJson<Payload>(url, { signal })} catch (error) { if (error instanceof ExternalApiClientError && error.status === 429) { // back off and retry later } throw error}Destinations and telemetry
Section titled “Destinations and telemetry”Moltaro imposes no destination allowlist. External requests are subject to the normal browser and network policy of the deployment: the destination must allow the WebApp origin via CORS, HTTPS pages cannot make mixed-content HTTP calls, and TLS validation applies as usual.
Requests made through the client are counted in Workspace UI runtime
telemetry — status family and duration only, never URLs, headers, or bodies —
and surface through
Monitoring.
Direct fetch, WebSocket, and EventSource remain available to page code,
but calls made that way are not captured by Moltaro telemetry.
Where API keys belong
Section titled “Where API keys belong”Confidential credentials — paid API keys, partner secrets, anything the
workspace must not expose — belong in server-side
C# business logic, where the
key stays on the host and the page calls your command or function through
MoltaroApiClient. Keys that are public by design, or supplied by the user
at runtime for their own account, may be used in page code deliberately.
Worked example: Frankfurter exchange rates
Section titled “Worked example: Frankfurter exchange rates”The UI Studio page generator ships an external-API example page built on
the free Frankfurter service (api.frankfurter.dev, no key required). Its
API module, adapted from the generated source:
import { ExternalApiClient } from '@moltaro/workspace-ui'
export interface ExchangeRate { base: string date: string quote: string rate: number}
const api = new ExternalApiClient()
export async function loadExchangeRates ( base: string, quotes: string[], signal: AbortSignal) { const query = new URLSearchParams({ base, quotes: quotes.join(',') }) const result = await api.requestJson<unknown>( 'https://api.frankfurter.dev/v2/rates?' + query.toString(), { signal, credentials: 'omit' }) if (!Array.isArray(result)) { throw new TypeError('Frankfurter returned an invalid rates response.') } return result as ExchangeRate[]}The generated example validates the response shape before trusting it and
never assumes an external API honors its own contract. Its Pinia store wires
an AbortController into signal so a newer refresh cancels the in-flight
one, and treats the resulting AbortError as silence rather than a failure —
the same pattern described in
Pages, components, and stores.