API · Public API + client

A public API projected from the app's own procedures — REST under /v1 with the standard profile, typed errors, pagination, Idempotency-Key — with an OpenAPI spec + Swagger UI and a generated TypeScript client in both flavors (Promise without Effect, Effect service) that voltro dev keeps written and you publish to npm. Zero-infra boot.

The public API that a partner programs against, built without a second definition of anything: three procedures carry publicApi and become GET /v1/products (paginated), GET /v1/products/{productId} (a typed 404) and POST /v1/products (scope-guarded, Idempotency-Key). The same descriptors produce the OpenAPI document, Swagger UI, and — what this template exists to show — the generated TypeScript client a consumer installs from npm. Boots with zero infra (store: 'memory'). Template id: api-public.

Beside it: api-rest is the same surface hand-written with defineRestRoute; api-backend is the reactive rpc side.

What you get

Piece Where
The catalog database/schema.tsproducts with timestampMs on the wire
The surface queries/products.list.query.ts, products.get.query.ts, mutations/products.create.mutation.tspublicApi: { resource: 'products', … }, errorStatus, idempotent, an authored example
The wire shape and the typed error lib/product.ts — browser-safe, imported by every descriptor
The decision app.config.tspublicApi.profile, publicApi.client, artifacts: ['openapi', 'typescript']
The spec /openapi.json + /docs, from the same projections (includeAppProcedures)
The client sdk/typescript/ — written by voltro dev, @acme/api-client at . (Promise, no Effect) and ./effect
Tests tests/products.test.ts — the executors against the in-memory store, the projection read off the descriptors

The client, in both flavors

import { createClient, ProductNotFound } from '@acme/api-client'

const api = createClient({ baseUrl: 'https://api.example.com', token: process.env.API_KEY })
const page = await api.products.list({ limit: 20 })          // items[].createdAt is a Date
for await (const p of api.products.list.pages()) { /* every page, Link walked for you */ }
try { await api.products.get({ productId: 'prod_missing' }) } catch (e) { if (e instanceof ProductNotFound) { /* 404, typed */ } }
await api.products.create({ name: 'Widget', priceCents: 1999 }) // Idempotency-Key minted per call
for await (const live of api.products.list.subscribe()) { /* the current page and every change, over the socket */ }
import { Effect } from 'effect'
import { ApiClient } from '@acme/api-client/effect'

const program = Effect.gen(function* () {
  const api = yield* ApiClient
  return yield* api.products.list()
}).pipe(Effect.catchTag('ProductNotFound', () => Effect.succeed({ items: [], nextCursor: null })), Effect.provide(ApiClient.layer({ baseUrl, token })))

The consumer picks: . pulls in no Effect at all; ./effect needs effect and @voltro/api-client-effect, declared as optional peers.

Change the decision, publish

voltro api-client show                      # the decision, defaults filled in
voltro api-client set --flavor plain        # the Promise client only — no Effect in the package
voltro api-client build                     # write sdk/typescript now
cd sdk/typescript && pnpm install && pnpm build && pnpm publish --access public

set edits publicApi.client in app.config.ts through the TypeScript AST; the voltro api-client page has every flag.

Auth, honestly

A single dev-only API key (publicdemo_devkey, its SHA-256 in app.config.ts) makes the guarded mutation work the moment you boot. It is a sample password: delete it before anything real, and resolve the hash against your own key store — or turn on the framework's first-class API keys (apiKeys: true).