TypeScript
A guide to using Soxom-generated TypeScript SDKs. The generated package is
isomorphic (Node 18+, modern browsers, Bun, Deno, Cloudflare Workers), ships
both ESM and CJS builds with .d.ts types, and has zero runtime dependencies —
all networking is built on the platform fetch and ReadableStream primitives.
Installation
Section titled “Installation”The package name is whatever you set in targets.typescript.package_name. For
the examples below we assume @acme/sdk:
npm install @acme/sdk# oryarn add @acme/sdk# orpnpm add @acme/sdkQuickstart
Section titled “Quickstart”import { Client } from "@acme/sdk";
// Bearer token from the ACME_BEARER_TOKEN env var (default name).const client = new Client();
const user = await client.users.get("user-123");console.log(user.email);The constructor reads credentials from the matching environment variable when the option is omitted. Pass them explicitly when you need to:
const client = new Client({ bearerToken: process.env.ACME_BEARER_TOKEN, baseURL: "https://api.acme.com", timeout: 30_000, // ms; default 60_000 maxRetries: 2, // default 2});Authentication
Section titled “Authentication”The constructor option name and env-var name come from the SDK’s configured
auth scheme (see authentication in
the soxom.yaml reference). <PREFIX> below is the screaming-snake-case of
sdk.name (e.g. acme-sdk → ACME). Custom env-var names override the
defaults.
Bearer token
Section titled “Bearer token”const client = new Client({ bearerToken: "sk_...", // or omit to read from ACME_BEARER_TOKEN});| Constructor option | Default env var |
|---|---|
bearerToken | <PREFIX>_BEARER_TOKEN |
API key (header or query)
Section titled “API key (header or query)”const client = new Client({ apiKey: "sk_...", // or omit to read from ACME_API_KEY});| Constructor option | Default env var |
|---|---|
apiKey | <PREFIX>_API_KEY |
The injection location (header name vs. query parameter) is fixed at generation time from the OpenAPI security scheme.
Basic auth
Section titled “Basic auth”const client = new Client({ username: "...", // or read from ACME_USERNAME password: "...", // or read from ACME_PASSWORD});| Constructor option | Default env var |
|---|---|
username | <PREFIX>_USERNAME |
password | <PREFIX>_PASSWORD |
OAuth2
Section titled “OAuth2”const client = new Client({ clientId: "...", // or ACME_CLIENT_ID clientSecret: "...", // or ACME_CLIENT_SECRET scope: "read write", // optional});| Constructor option | Default env var |
|---|---|
clientId | <PREFIX>_CLIENT_ID |
clientSecret | <PREFIX>_CLIENT_SECRET |
The client_credentials grant runs lazily on the first request. Tokens are
cached in memory until 30s before their expires_in, refreshes in flight are
coalesced into a single token request, and a 401 from the API triggers exactly
one forced refresh + retry.
Making requests
Section titled “Making requests”Operations are mounted under resource and subresource namespaces as configured
in soxom.yaml:
await client.users.list();await client.users.get("user-123");await client.users.settings.update("user-123", { theme: "dark" });Every method accepts a final RequestOptions argument for per-call overrides:
await client.reports.generate(params, { timeout: 120_000, // override the client default maxRetries: 0, // disable retries for this call signal: AbortSignal.timeout(5_000), // user-supplied AbortSignal headers: { "X-Trace-Id": "abc" }, // merged on top of defaults idempotencyKey: "order-2024-01-01-001", // explicit Idempotency-Key query: { include: "deleted" }, // extra query params});Pagination
Section titled “Pagination”List operations return a CursorPage or OffsetPage (selected by the
pagination.default_type in soxom.yaml). Both implement AsyncIterable, so
the simplest consumer transparently walks every page:
for await (const user of await client.users.list()) { console.log(user.email);}For manual control, hold on to the page object:
const page = await client.users.list({ limit: 50 });for (const user of page.data) { console.log(user.email);}
while (page.hasNextPage()) { const next = await page.getNextPage(); // ... process next.data}Streaming
Section titled “Streaming”Operations marked streaming in soxom.yaml return a Stream<T> that is also
an async iterable, parsed lazily from the response body as Server-Sent Events:
const stream = await client.chat.completions.create({ model: "gpt-4", messages: [{ role: "user", content: "Hello" }], stream: true,});
for await (const event of stream) { process.stdout.write(event.choices[0]?.delta?.content ?? "");}Cancel an in-flight stream via its bound AbortController:
setTimeout(() => stream.controller.abort(), 5_000);Errors
Section titled “Errors”All thrown errors descend from SoxomError, so a single instanceof check
will catch anything originating in the SDK. The hierarchy:
SoxomError└── APIError ├── APIConnectionError │ ├── APIConnectionTimeoutError │ └── APIUserAbortError └── APIStatusError ├── BadRequestError (400) ├── AuthenticationError (401) ├── PermissionDeniedError (403) ├── NotFoundError (404) ├── ConflictError (409) ├── UnprocessableEntityError (422) ├── RateLimitError (429) └── InternalServerError (5xx)import { RateLimitError, AuthenticationError, APIStatusError, APIConnectionError,} from "@acme/sdk";
try { await client.users.get("user-123");} catch (err) { if (err instanceof RateLimitError) { console.warn("rate limited; retry-after:", err.headers?.get?.("retry-after")); } else if (err instanceof AuthenticationError) { console.error("token rejected"); } else if (err instanceof APIStatusError) { console.error(`HTTP ${err.status}`, err.error, "request id:", err.requestId); } else if (err instanceof APIConnectionError) { console.error("network failure:", err.message); } else { throw err; }}APIStatusError carries status, headers, error (the parsed response
body), and requestId (from the x-request-id or request-id header).
Retries
Section titled “Retries”The runtime retries on connection errors, request timeouts, and HTTP 408,
409, 429, 500, 502, 503, 504. Backoff is exponential with full
jitter, capped at 30s, and respects a parseable Retry-After header (either
delta-seconds or HTTP-date). The default budget is 2 attempts; configure via
the constructor or per-request:
const client = new Client({ maxRetries: 5 });
// or per-request:await client.payments.create(data, { maxRetries: 0 });Retries of unsafe methods (POST, PATCH) are gated on having an idempotency
key. By default the runtime auto-generates one on the second attempt
(idempotency: "auto"); set idempotency: "always" to send a key on the
first try too, or supply your own via RequestOptions.idempotencyKey.
Configuration in soxom.yaml
Section titled “Configuration in soxom.yaml”targets: typescript: package_name: "@acme/sdk" # npm package name; default is kebab-case of sdk.name module_format: "esm" # "esm" (default) or "cjs" generator_version: "0.1.0" # pin the generator versionSee the soxom.yaml reference for the complete option list.
Runtime requirements
Section titled “Runtime requirements”- Node.js 18 or newer (uses the platform
fetch,ReadableStream, andAbortSignal). - Modern browsers, Bun, Deno, Cloudflare Workers — fully isomorphic, no Node-only imports.
Build & publish
Section titled “Build & publish”The generated package is a tsup-based npm module:
npm installnpm run build # emits dist/index.js (ESM), dist/index.cjs (CJS), dist/index.d.tsnpm publish