Skip to content

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.

The package name is whatever you set in targets.typescript.package_name. For the examples below we assume @acme/sdk:

Terminal window
npm install @acme/sdk
# or
yarn add @acme/sdk
# or
pnpm add @acme/sdk
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
});

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-sdkACME). Custom env-var names override the defaults.

const client = new Client({
bearerToken: "sk_...", // or omit to read from ACME_BEARER_TOKEN
});
Constructor optionDefault env var
bearerToken<PREFIX>_BEARER_TOKEN
const client = new Client({
apiKey: "sk_...", // or omit to read from ACME_API_KEY
});
Constructor optionDefault env var
apiKey<PREFIX>_API_KEY

The injection location (header name vs. query parameter) is fixed at generation time from the OpenAPI security scheme.

const client = new Client({
username: "...", // or read from ACME_USERNAME
password: "...", // or read from ACME_PASSWORD
});
Constructor optionDefault env var
username<PREFIX>_USERNAME
password<PREFIX>_PASSWORD
const client = new Client({
clientId: "...", // or ACME_CLIENT_ID
clientSecret: "...", // or ACME_CLIENT_SECRET
scope: "read write", // optional
});
Constructor optionDefault 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.

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
});

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
}

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);

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).

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.

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 version

See the soxom.yaml reference for the complete option list.

  • Node.js 18 or newer (uses the platform fetch, ReadableStream, and AbortSignal).
  • Modern browsers, Bun, Deno, Cloudflare Workers — fully isomorphic, no Node-only imports.

The generated package is a tsup-based npm module:

Terminal window
npm install
npm run build # emits dist/index.js (ESM), dist/index.cjs (CJS), dist/index.d.ts
npm publish