Skip to content

About

hey-api experimental SDK

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@valtown/sdk-openapi-ts

A port of the official Val Town TypeScript SDK that is generated with Hey API's openapi-ts instead of Stainless. The call style is kept as close to the original as the generator allows, so migrating between the two is mostly a drop-in.

Install

npm install @valtown/sdk-openapi-ts

Requires Node.js >= 20 (any runtime with a global fetch).

Usage

import ValTown from '@valtown/sdk-openapi-ts';

const client = new ValTown({
  bearerToken: process.env['VAL_TOWN_API_KEY'], // default: VAL_TOWN_API_KEY env var
});

const response = await client.emails.send({ body: 'hi' });
console.log(response.message);

Resource tree parity

Operation IDs in the source spec are rewritten (see scripts/transform-spec.mjs) to dotted paths that mirror the original SDK's structure, and openapi-ts groups them into nested containers. The following call styles match the original exactly:

Original SDK This port
client.me.profile.retrieve() client.me.profile.retrieve()
client.me.vals.list({ offset, limit }) client.me.vals.list({ offset, limit })
client.emails.send({ ... }) client.emails.send({ ... })
client.vals.retrieve(valID) client.vals.retrieve({ val_id })
client.vals.branches.create(valID, {...}) client.vals.branches.create({ val_id, ... })
client.alias.username.retrieve(username) client.alias.username.retrieve({ username })
client.orgs.memberships.list(orgID) client.orgs.memberships.list({ org_id })
client.blobs.store(key, {...}) client.blobs.store({ key, ... })
client.sqlite.execute({ ... }) client.sqlite.execute({ ... })
client.token.slack({ ... }) client.token.slack({ ... })

Positions vs. options objects. The original SDK passes path parameters positionally (retrieve(valID)); openapi-ts always takes a flat options object (retrieve({ val_id })). This is the one unavoidable call-style difference across every open-source generator we evaluated — see Why openapi-ts below.

Errors

Non-2xx responses throw the same typed errors as the original:

try {
  await client.emails.send({ body: 'hi' });
} catch (err) {
  if (err instanceof ValTown.APIError) {
    console.log(err.status);      // e.g. 400
    console.log(err.name);        // e.g. BadRequestError
    console.log(err.headers);     // Headers
  }
}

Status → class: 400 BadRequestError, 401 AuthenticationError, 403 PermissionDeniedError, 404 NotFoundError, 422 UnprocessableEntityError, 429 RateLimitError, >=500 InternalServerError. Network failures become APIConnectionError / APIConnectionTimeoutError.

Retries & timeouts

Requests are retried 2× by default (exponential backoff with jitter) on connection errors and 408/409/429/5xx, respecting Retry-After. Requests time out after 60s by default.

const client = new ValTown({ maxRetries: 0, timeout: 20_000 });

Auth

Pass a token via bearerToken or the VAL_TOWN_API_KEY environment variable. Only endpoints that the spec marks as authenticated send Authorization: Bearer <token> — public endpoints (e.g. aliases, val lookup) do not, which is faithful to the API.

Auto-pagination

The generated list methods return the raw { data, links } result set. To get the original SDK's auto-pagination call style, wrap the list call with client.paginate(...) — the returned value is a PagePromise that is itself async-iterable, exactly like the original's unawaited for await:

// Walks every page via `links.next`, yielding individual items:
for await (const val of client.paginate(client.vals.list, { limit: 20 })) {
  console.log(val.id);
}

// Or work with pages explicitly:
const page = await client.paginate(client.vals.list, { limit: 20 });
for (const val of page.data) console.log(val.id);
while (page.hasNextPage()) {
  page = await page.getNextPage();
}

hasNextPage() is true only while there are items and a links.next (terminal detection matches the original), and follow-up page requests reuse the same client — auth headers, retries, timeouts, and error mapping all apply.

Undocumented endpoints

const data = await client.get('/v1/some/path', { query: { limit: 10 } });
// client.post / put / patch / delete also available

Full type access

Generated response/param types are exported with <Operation>Response / <Operation>Data names (e.g. EmailsSendResponse), plus shared schemas (Val, User, Org, ResultSet, …):

import ValTown, { type Val, type ResultSet } from '@valtown/sdk-openapi-ts';
const val: Val = await client.vals.retrieve({ val_id: '1' });

Regeneration

The SDK is fully generated from the pinned spec in openapi.json.

npm run spec:fetch      # download the latest spec from api.val.town (updates openapi.json)
npm run spec:transform  # rewrite operationIds into the parity dotted paths
npm run generate        # run openapi-ts -> src/client/
npm run verify          # typecheck + test + build

src/client/ is generated and checked in (so consumers don't need the generator), but should not be edited by hand. New operations added upstream by Val Town will fall back to a deterministic auto-name until someone adds an explicit entry to the OPERATION_IDS table in scripts/transform-spec.mjs.

CI regeneration + release automation

.github/workflows/regenerate.yml runs on a weekly schedule (and via manual dispatch):

  1. downloads the live spec,
  2. regenerates src/client/,
  3. verifies (typecheck, tests, build),
  4. commits + pushes any changes to main, and
  5. runs release-please which queues a release: vX.Y.Z pull request incorporating the regeneration.

Merging that PR creates the release tag; .github/workflows/publish.yml publishes the package to npm.

The .github/ directory lives at this repository's root: regenerate.yml pushes regenerated code to main and then runs release-please from here.

Why openapi-ts

We evaluated the main open-source TypeScript generators against the goal of matching the Stainless call style (nested client.me.vals.list() tree, flat data returns, typed errors):

  • openapi-ts is the only one that emits a class-per-nesting-segment client (client.me.vals.list()) directly from the spec, with strategy: 'single' + nesting: 'operationId' — no handwritten resource shell.
  • openapi-typescript + openapi-fetch generates types only and is URL-centric (client.GET('/v2/me/vals')); reproducing the resource tree would mean handwriting all 52 methods.
  • orval is React Query–centric; its plain-fetch mode emits per-endpoint functions and needs a custom plugin for class-style output.
  • swagger-typescript-api can do per-tag classes via custom templates, but is less actively maintained and harder to bend to the exact tree.

The one gap common to all of them — positional path params like retrieve(valID) — we accept and document rather than hand-maintain wrappers over the whole generated surface.

Differences from @valtown/sdk

Area Original (Stainless) This port (openapi-ts)
Path params positional (retrieve(valID)) options object ({ val_id })
Pagination Page w/ hasNextPage()/getNextPage() raw { data, links } + client.paginate() for-await
Response type names ValTown.EmailSendResponse EmailsSendResponse (+ alias)
.asResponse() available on APIPromise via client.raw / raw verbs
Runtime dual CJS/ESM across many runtimes ESM + CJS, Node >= 20

License

MIT

About

hey-api experimental SDK

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages