ts-procedures
v11.0.0
Published
A TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition. Define your procedures once and get full type inference, runtime validation, and framework integration hooks.
Downloads
2,957
Readme
ts-procedures
A TypeScript RPC framework that creates type-safe, schema-validated procedure calls from a single function definition. Define procedures once on the server and get full type inference, runtime validation, HTTP serving, and generated clients (TypeScript, Kotlin, Swift) — with typed errors end to end.
import { Type } from 'typebox'
import { Procedures } from 'ts-procedures'
type Ctx = { db: Db }
const { Create } = Procedures<Ctx>()
export const { GetUser } = Create(
'GetUser',
{
schema: {
params: Type.Object({ id: Type.String() }),
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
},
},
async (ctx, params) => {
const user = await ctx.db.users.find(params.id)
if (!user) throw ctx.error('User not found', { id: params.id })
return user
},
)
// Directly callable (great for tests), fully typed:
await GetUser({ db }, { id: 'u1' })The four procedure kinds
| Creator | Kind | Shape |
|---|---|---|
| Create | rpc | (ctx, params) => Promise<T> — POST, body in/JSON out |
| CreateStream | rpc-stream | async generator — SSE (or text) stream |
| CreateHttp | http | REST route with per-channel input (pathParams, query, body, headers) |
| CreateHttpStream | http-stream | REST route streaming SSE, optional initial headers |
Every creator returns { [name]: handler, procedure: handler, info }: the
named handler for direct calls, and info for introspection (computed JSON
Schema, validators, your extended config).
const { CreateHttp } = Procedures<Ctx>({ http: { pathPrefix: '/v1', scope: 'users' } })
export const { UpdateUser } = CreateHttp(
'UpdateUser',
{
path: '/users/:id',
method: 'put',
errors: ['NotFound'], // taxonomy keys → typed client errors
schema: {
req: {
pathParams: Type.Object({ id: Type.String() }),
body: Type.Object({ name: Type.String() }),
},
res: { body: Type.Object({ id: Type.String(), name: Type.String() }) },
},
},
async (ctx, req) => ctx.db.users.update(req.pathParams.id, req.body),
)Serving over HTTP (Hono)
import { Hono } from 'hono'
import { HonoAppBuilder, defineErrorTaxonomy } from 'ts-procedures/hono'
const errors = defineErrorTaxonomy({
NotFound: { class: NotFoundError, statusCode: 404 },
})
const builder = new HonoAppBuilder({ pathPrefix: '/api', errors })
.register(factory, (c) => ({ db: makeDb(c) })) // context per request (sync or async)
const app = builder.build() // a Hono app — mount anywhere Hono runsOne builder serves all four kinds. Config is stratified: kind-specific blocks
(rpc.onSuccess, api.queryParser, stream.defaultStreamMode /
onStreamStart / onStreamEnd / onMidStreamError) plus cross-cutting
error handling (errors taxonomy, unknownError, imperative onError,
onRequestError observer) and lifecycle (onRequestStart/End).
An Astro adapter ships too: createAstroHandler from ts-procedures/astro
serves built Hono apps from an Astro catch-all route.
Error taxonomy
Declarative, typo-proof error handling: map error classes (or predicates) to status codes and wire bodies once, and the same source of truth drives runtime responses, envelope docs, and generated typed client errors.
const errors = defineErrorTaxonomy({
NotFound: { class: NotFoundError, statusCode: 404 },
UseCase: { class: UseCaseError, statusCode: 422, toResponse: (e) => ({ message: e.publicMessage }) },
PgUnique: { match: (e): e is DatabaseError => isPgError(e, '23505'), statusCode: 409 },
})Subclasses are checked before base classes automatically (topological sort);
{ name } is injected into bodies so client dispatch always works; framework
defaults (ProcedureValidationError → 400, etc.) layer underneath yours.
Generated clients
Codegen consumes a DocEnvelope — from a live URL or a file written with
writeDocEnvelope(builder, 'envelope.json'):
npx ts-procedures-codegen --url http://localhost:3000/api/docs --out src/generated
# or offline:
npx ts-procedures-codegen --file envelope.json --out src/generatedimport { createApiClient } from './generated'
const api = createApiClient({ basePath: 'https://api.example.com' })
const user = await api.users.GetUser({ id: 'u1' }) // throws typed errors
const result = await api.users.GetUser.safe({ id: 'u1' }) // Result<T, E> instead
for await (const event of api.users.WatchUsers({})) { ... } // TypedStream- Typed errors: routes declaring
errors: [...]get real error classes —catch (e) { if (e instanceof ApiErrors.NotFound) ... }— plus a namedErrorsunion per route. - Self-contained by default: the generated directory bundles its own
runtime (
_client.ts/_types.ts); no runtime dependency on this package. - Shared models: schemas carrying
$idare hoisted once into_models.ts(or re-exported from your own package via--shared-models-module). - Kotlin / Swift:
--target kotlin --kotlin-package com.example.apior--target swiftemit types + route constants/path builders for mobile teams (they own the HTTP layer). - Watch mode, config file (
ts-procedures-codegen.config.json), strict flags with did-you-mean, orphaned-file pruning — seenpx ts-procedures-codegen --help.
Validation & schemas
- TypeBox is built in (
import { Type } from 'typebox');schema.params/schema.req.*are validated at runtime with AJV (allErrors,coerceTypes,removeAdditional);schema.returnType/schema.resdocument and drive codegen. - Customize AJV per factory:
Procedures({ validation: { ajv: {...} } }). - Skip per-call validation for trusted internal factories:
Procedures({ validation: false })(schemas still computed; bad schemas still fail fast at registration). - Plug in another schema library with a 3-line
SchemaAdapter(Procedures({ schema: { adapters: [zodAdapter] } })). - Factory middleware (ctx + input aware, runs for all procedures):
Procedures({ middleware: [({ ctx, input, next }) => ...] }).
Streaming
Stream handlers always receive ctx.signal — it aborts on client disconnect,
and with reason 'stream-completed' on normal completion. Yields become SSE
events (customize with sse(data, { event, id, retry })); the generator's
return value arrives as the event: 'return' payload, surfaced by generated
clients as await stream.result. Opt into per-yield validation with
validateYields: true.
Build your own server adapter
Everything the Hono adapter uses lives in ts-procedures/server, framework-free:
route-doc builders, taxonomy dispatch (data in, data out), request channel
extraction (RequestSource), SSE metadata, DocRegistry. A Fastify or Express
adapter is a few thin handlers — src/adapters/hono/ is the reference
implementation.
Subpaths
| Import | Contents |
|---|---|
| ts-procedures | Procedures, error classes, schema utilities, writeDocEnvelope |
| ts-procedures/hono | HonoAppBuilder, defineErrorTaxonomy, sse, DocRegistry |
| ts-procedures/astro | createAstroHandler, getAstroContext |
| ts-procedures/server | Transport-agnostic adapter toolkit |
| ts-procedures/http | HTTP doc/config types (type-only) |
| ts-procedures/http-docs | DocRegistry |
| ts-procedures/http-errors | Error taxonomy helpers |
| ts-procedures/client | Runtime client (createClient, adapters, hooks, errors) |
| ts-procedures/codegen | generateClient programmatic API |
AI assistant setup
The package ships a single spec-compliant Agent Skill
so your AI tooling knows the framework's patterns and footguns. One skill is
installed into the standard agent directories (.agents/skills/ — read by
Codex, Cursor, Copilot, Gemini CLI and 30+ others — plus .claude/skills/ for
Claude Code):
npx ts-procedures-setup # install the skill
npx ts-procedures-setup --dry-run # preview
npx ts-procedures-setup --check # CI gate: fail if outdatedThe installed skill auto-updates on subsequent npm install via the package's
postinstall hook. See docs/ai-agent-setup.md.
Migrating from v8
Generated client output is byte-identical to v8.6.0 — consumers have nothing to do. Server-side breaking changes are small and mechanical; see docs/migration-v8-to-v9.md.
License
MIT
