Generate a TypeScript SDK from your OpenAPI spec

Octri reads your OpenAPI document and writes a TypeScript client with types from your schemas, publishes it to npm under your own package name, and rebuilds it every time the spec changes.

Install from npm
npm install @acme/api

The client they actually get

What your users write once the TypeScript package is installed.

Two call-site shapes come out of the same spec. class-namespaced is the default: a client object whose operations are grouped by resource, which is the surface every widely used SDK ships and the one your own reference docs already assume. functions is the override: no client object, one configure call, and each operation called directly. It is a single setting, per language, and nothing else about the client changes with it.

Types come from your schemas either way, so a renamed field shows up as a compile error in your users’ editors. Free functions tree-shake, which is the main reason a bundler-minded team picks them.

Scoped or unscoped, published under your own account. npm does not allow renaming and will not republish a deleted version, so the first release claims the name for good.

Client object (default)
import { Acme } from "@acme/api";

const acme = new Acme({
  baseUrl: "https://api.acme.com/v1",
  auth: { bearer: process.env.ACME_TOKEN },
});

// A paginated operation IS an async generator. The loop pulls the next page,
// and nothing is prefetched.
for await (const user of acme.users.list({ limit: 100 })) {
  console.log(user.id);
}

const invoice = await acme.invoices.create({
  customerId: "cus_123",
  amountCents: 4900,
});
Free functions
import { configureClient, listUsers, createInvoice } from "@acme/api";

configureClient({
  baseUrl: "https://api.acme.com/v1",
  auth: { bearer: process.env.ACME_TOKEN },
});

for await (const user of listUsers({ limit: 100 })) {
  console.log(user.id);
}

const invoice = await createInvoice({ customerId: "cus_123", amountCents: 4900 });

The package at a glance

What a developer checks before adding the TypeScript package to a project.

Runs on
Node 20+, browsers, Deno, Bun and edge runtimes
Transport
Fetch API by default, axios as an option
Runtime dependencies
None on the fetch engine
Models
TypeScript types generated from your schemas
Pagination
AsyncGenerator, consumed with for await
Ships to
npm, scoped or unscoped

When a call fails

Each common status has its own subclass of SdkHttpError, so instanceof narrows the type and the editor knows which fields exist. A response your spec declares an error model for arrives decoded on err.data.

Typed errors
import { SdkHttpError, SdkRateLimitError } from "@acme/api";

try {
  await acme.invoices.create({ customerId: "cus_123", amountCents: 4900 });
} catch (err) {
  if (err instanceof SdkRateLimitError) {
    // 429: the client already retried with backoff. Queue it for later.
  } else if (err instanceof SdkHttpError) {
    console.error(err.statusCode, err.requestId);
  } else {
    throw err;
  }
}

What you decide for TypeScript

Set project-wide, or per language when TypeScript wants something the others do not. Defaults are the shape TypeScript SDKs actually ship.

  • Client style

    class-namespaced (default), class, namespaced, functions

    A client object whose operations are grouped by resource is the default, because it is the surface every widely used SDK ships and the one your reference docs already assume. Free functions are the override, and they tree-shake.

  • HTTP engine

    fetch (default), axios

    fetch keeps the package dependency-free on Node 20+, browsers, Deno, Bun and edge runtimes. axios buys interceptors at the cost of a dependency in your users’ bundles.

  • Argument style

    object (default), positional

    An options object survives a new optional parameter. Positional arguments break on one.

Method naming and namespacing work the same in every language: operations are grouped by your spec’s tags or by path, and the namespace word is dropped from the method name unless you ask to keep the operationId whole.

The npm name is permanent

npm has no rename, and it refuses to republish a version you deleted. A scoped name (@acme/api) is namespaced to your organisation, cannot be squatted, and reads unambiguously beside your other packages. Settle it before the first publish.

npm package name
@acme/api
  • Not possible: Rename the package
  • Not possible: Republish a deleted version
  • Still possible: Deprecate a version with a message

The same in every generated client

The parts nobody wants to hand-write, and the parts a hand-written client usually skips.

  • Retries with exponential backoff and jitter, on the statuses that mean "try again", with a per-attempt timeout.
  • Idempotency keys on the methods that need them, so a retried write does not become two.
  • Auth wired in from your spec’s security scheme, read from wherever your users keep secrets.
  • Typed errors split by cause: a failed constraint, an HTTP status, a network failure and a timeout are four different things.
  • Pagination that follows whichever contract your spec declares, cursor, offset, page number or next URL, without prefetching the collection.
  • Server-sent events as a native stream, for the operations that stream.
  • Error reporting to Octri’s monitoring, pre-wired and switched off until a consumer opts in.
  1. Point it at your spec

    Upload an OpenAPI or Swagger document, paste a URL, or connect the repository it lives in so a merge to main updates it.

  2. Read the audit

    The spec is scored out of 10 on what a generator can do with it, with every missing schema and colliding operationId named. 8 of the fourteen rules carry a button that writes the fix.

  3. Shape the client

    Client style, argument style, namespacing, auth, retries and pagination, set once for the project and overridden per language or per endpoint.

  4. Generate and read it

    The package is generated, compiled and handed to you as source you can read before anyone installs it.

  5. Publish, then keep publishing

    Release to the registry under your own account. Every later spec change regenerates the client, and the version number comes from your own document.

Questions

It is TypeScript source. Types are generated from your schemas alongside the methods, so there is no hand-written declaration file to keep in step.