ts-procedures 5.7.2 → 5.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1051
- package/agent_config/claude-code/skills/guide/api-reference.md +21 -16
- package/agent_config/claude-code/skills/guide/patterns.md +3 -1
- package/agent_config/copilot/copilot-instructions.md +7 -5
- package/agent_config/cursor/cursorrules +7 -5
- package/build/codegen/bin/cli.d.ts +2 -0
- package/build/codegen/bin/cli.js +21 -10
- package/build/codegen/bin/cli.js.map +1 -1
- package/build/codegen/bin/cli.test.js +44 -2
- package/build/codegen/bin/cli.test.js.map +1 -1
- package/build/codegen/emit-errors.d.ts +4 -1
- package/build/codegen/emit-errors.js +11 -5
- package/build/codegen/emit-errors.js.map +1 -1
- package/build/codegen/emit-errors.test.js +37 -0
- package/build/codegen/emit-errors.test.js.map +1 -1
- package/build/codegen/emit-index.d.ts +3 -1
- package/build/codegen/emit-index.js +6 -13
- package/build/codegen/emit-index.js.map +1 -1
- package/build/codegen/emit-index.test.js +23 -0
- package/build/codegen/emit-index.test.js.map +1 -1
- package/build/codegen/emit-scope.js +17 -13
- package/build/codegen/emit-scope.js.map +1 -1
- package/build/codegen/emit-scope.test.js +166 -0
- package/build/codegen/emit-scope.test.js.map +1 -1
- package/build/codegen/index.d.ts +1 -0
- package/build/codegen/index.js +1 -0
- package/build/codegen/index.js.map +1 -1
- package/build/codegen/naming.d.ts +7 -0
- package/build/codegen/naming.js +21 -0
- package/build/codegen/naming.js.map +1 -0
- package/build/codegen/naming.test.d.ts +1 -0
- package/build/codegen/naming.test.js +40 -0
- package/build/codegen/naming.test.js.map +1 -0
- package/build/codegen/pipeline.d.ts +1 -0
- package/build/codegen/pipeline.js +7 -3
- package/build/codegen/pipeline.js.map +1 -1
- package/build/codegen/pipeline.test.js +60 -0
- package/build/codegen/pipeline.test.js.map +1 -1
- package/docs/ai-agent-setup.md +61 -0
- package/docs/client-and-codegen.md +193 -0
- package/docs/core.md +473 -0
- package/docs/http-integrations.md +183 -0
- package/docs/streaming.md +199 -0
- package/docs/superpowers/plans/2026-03-30-client-codegen.md +2833 -0
- package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +632 -0
- package/package.json +6 -1
- package/src/implementations/http/README.md +324 -0
- package/src/implementations/http/express-rpc/README.md +281 -0
- package/src/implementations/http/hono-rpc/README.md +358 -0
- package/src/implementations/http/hono-stream/README.md +525 -0
package/docs/core.md
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
[Home](../README.md) | **Core** | [Streaming](./streaming.md) | [HTTP Integrations](./http-integrations.md) | [Client & Codegen](./client-and-codegen.md) | [AI Agent Setup](./ai-agent-setup.md)
|
|
2
|
+
|
|
3
|
+
# Core Procedures
|
|
4
|
+
|
|
5
|
+
ts-procedures creates type-safe, schema-validated procedure calls with a single function definition. Define your procedures once and get full type inference, runtime validation, and procedure documentation/configuration.
|
|
6
|
+
|
|
7
|
+
## Procedures Factory
|
|
8
|
+
|
|
9
|
+
The `Procedures()` function creates a factory for defining procedures. It accepts two generic type parameters:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
Procedures<TContext, TExtendedConfig>(builder?: {
|
|
13
|
+
onCreate?: (procedure: TProcedureRegistration<TContext, TExtendedConfig>) => void
|
|
14
|
+
})
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| Parameter | Description |
|
|
18
|
+
|-----------|----------------------------------------------------------------------------|
|
|
19
|
+
| `TContext` | The base context type passed to all handlers as the first parameter |
|
|
20
|
+
| `TExtendedConfig` | Additional configuration properties for all procedures `config` properties |
|
|
21
|
+
| `builder.onCreate` | Optional callback invoked when each procedure is registered (runtime) |
|
|
22
|
+
|
|
23
|
+
## Create Function
|
|
24
|
+
|
|
25
|
+
The `Create` function defines individual procedures:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
Create(name, config, handler)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Returns:**
|
|
32
|
+
- `{ [name]: handler }` - Named export for the handler
|
|
33
|
+
- `procedure` - Generic reference to the handler
|
|
34
|
+
- `info` - Procedure meta (name, description, schema, `TExtendedConfig` properties, etc.)
|
|
35
|
+
|
|
36
|
+
## Structured Input with schema.input
|
|
37
|
+
|
|
38
|
+
For HTTP APIs and other multi-channel transports, `schema.input` provides per-channel type safety. Each key is an independently validated input channel:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
const { Create } = Procedures<AppContext, APIConfig>()
|
|
42
|
+
|
|
43
|
+
const { UpdateUser } = Create(
|
|
44
|
+
'UpdateUser',
|
|
45
|
+
{
|
|
46
|
+
path: '/users/:id',
|
|
47
|
+
method: 'put',
|
|
48
|
+
schema: {
|
|
49
|
+
input: {
|
|
50
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
51
|
+
query: Type.Object({ notify: Type.Optional(Type.Boolean()) }),
|
|
52
|
+
body: Type.Object({ name: Type.String(), email: Type.String() }),
|
|
53
|
+
},
|
|
54
|
+
returnType: Type.Object({ ok: Type.Boolean() }),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
async (ctx, { pathParams, query, body }) => {
|
|
58
|
+
// Each channel is independently typed and validated
|
|
59
|
+
await updateUser(pathParams.id, body)
|
|
60
|
+
if (query.notify) await sendNotification(pathParams.id)
|
|
61
|
+
return { ok: true }
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Rules:**
|
|
67
|
+
- `schema.input` and `schema.params` are **mutually exclusive** — defining both throws `ProcedureRegistrationError`
|
|
68
|
+
- Each channel is validated independently with per-channel error messages
|
|
69
|
+
- Works with both `Create` and `CreateStream`
|
|
70
|
+
|
|
71
|
+
## CreateStream Function
|
|
72
|
+
|
|
73
|
+
The `CreateStream` function defines streaming procedures that yield values over time using async generators:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
CreateStream(name, config, handler)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Config Options:**
|
|
80
|
+
- `schema.params` - Input parameter schema (validated at runtime)
|
|
81
|
+
- `schema.yieldType` - Schema for each yielded value (validated if `validateYields: true`)
|
|
82
|
+
- `schema.returnType` - Schema for final return value (documentation only)
|
|
83
|
+
- `validateYields` - Enable runtime validation of yielded values (default: `false`)
|
|
84
|
+
|
|
85
|
+
**Handler Signature:**
|
|
86
|
+
```typescript
|
|
87
|
+
async function* (ctx, params) => AsyncGenerator<TYield, TReturn | void>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Context Extensions (all handlers):**
|
|
91
|
+
- `ctx.error(message, meta?)` - Create a ProcedureError
|
|
92
|
+
- `ctx.signal?` - AbortSignal for cancellation support (optional for `Create`, always present for `CreateStream`)
|
|
93
|
+
|
|
94
|
+
When using the built-in HTTP implementations (Hono, Express), `ctx.signal` is automatically injected from the HTTP request, so handlers can detect client disconnection. For direct usage without a server, `signal` is `undefined` unless you pass one in context.
|
|
95
|
+
|
|
96
|
+
**Returns:**
|
|
97
|
+
- `{ [name]: handler }` - Named generator export
|
|
98
|
+
- `procedure` - Generic reference to the generator
|
|
99
|
+
- `info` - Procedure meta with `isStream: true`
|
|
100
|
+
|
|
101
|
+
For detailed streaming documentation (yield validation, abort signals, SSE integration), see [Streaming Procedures](./streaming.md).
|
|
102
|
+
|
|
103
|
+
## Using Generics
|
|
104
|
+
|
|
105
|
+
### Base Context
|
|
106
|
+
|
|
107
|
+
Define a shared context type for all procedures in your application:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
interface AppContext {
|
|
111
|
+
authToken: string
|
|
112
|
+
requestId: string
|
|
113
|
+
logger: Logger
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { Create } = Procedures<AppContext>()
|
|
117
|
+
|
|
118
|
+
const { SecureEndpoint } = Create(
|
|
119
|
+
'SecureEndpoint',
|
|
120
|
+
{},
|
|
121
|
+
async (ctx, params) => {
|
|
122
|
+
// ctx.authToken is typed as string
|
|
123
|
+
// ctx.requestId is typed as string
|
|
124
|
+
// ctx.logger is typed as Logger
|
|
125
|
+
return { token: ctx.authToken }
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
// When calling, you must provide the context
|
|
130
|
+
await SecureEndpoint({ authToken: 'abc', requestId: '123', logger: myLogger }, {})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Extended Configuration
|
|
134
|
+
|
|
135
|
+
Add custom properties to all procedure configs:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
interface ExtendedConfig {
|
|
139
|
+
permissions: string[]
|
|
140
|
+
rateLimit?: number
|
|
141
|
+
cacheTTL?: number
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const { Create } = Procedures<AppContext, ExtendedConfig>()
|
|
145
|
+
|
|
146
|
+
const { AdminOnly } = Create(
|
|
147
|
+
'AdminOnly',
|
|
148
|
+
{
|
|
149
|
+
permissions: ['admin'], // Required by ExtendedConfig
|
|
150
|
+
rateLimit: 100, // Optional
|
|
151
|
+
description: 'Admin-only endpoint',
|
|
152
|
+
},
|
|
153
|
+
async (ctx, params) => {
|
|
154
|
+
return { admin: true }
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// Access extended config via info
|
|
159
|
+
console.log(AdminOnly.info.permissions) // ['admin']
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Combined Example
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
interface CustomContext {
|
|
166
|
+
authToken: string
|
|
167
|
+
tenantId: string
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface ExtendedConfig {
|
|
171
|
+
requiresAuth: boolean
|
|
172
|
+
auditLog?: boolean
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const { Create, getProcedures } = Procedures<CustomContext, ExtendedConfig>({
|
|
176
|
+
onCreate: (procedure) => {
|
|
177
|
+
// Register with your framework
|
|
178
|
+
console.log(`Registered: ${procedure.name}`)
|
|
179
|
+
console.log(`Requires Auth: ${procedure.config.requiresAuth}`)
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const { CreateUser } = Create(
|
|
184
|
+
'CreateUser',
|
|
185
|
+
{
|
|
186
|
+
requiresAuth: true,
|
|
187
|
+
auditLog: true,
|
|
188
|
+
description: 'Creates a new user',
|
|
189
|
+
schema: {
|
|
190
|
+
params: Type.Object({
|
|
191
|
+
email: Type.String(),
|
|
192
|
+
name: Type.String(),
|
|
193
|
+
}),
|
|
194
|
+
returnType: Type.Object({ id: Type.String() }),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
async (ctx, params) => {
|
|
198
|
+
// Both context and params are fully typed
|
|
199
|
+
return { id: 'user-123' }
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Schema Validation
|
|
205
|
+
|
|
206
|
+
ts-procedures supports two schema libraries — **TypeBox** (recommended) and **Suretype**. The library is auto-detected from the schema object. Both work identically at runtime; choose whichever you prefer:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// TypeBox
|
|
210
|
+
import { Type } from 'typebox'
|
|
211
|
+
schema: { params: Type.Object({ title: Type.String() }) }
|
|
212
|
+
|
|
213
|
+
// Suretype
|
|
214
|
+
import { v } from 'suretype'
|
|
215
|
+
schema: { params: v.object({ title: v.string().required() }) }
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Validation Behavior
|
|
219
|
+
|
|
220
|
+
AJV is configured with:
|
|
221
|
+
- `allErrors: true` - Report all validation errors
|
|
222
|
+
- `coerceTypes: true` - Automatically coerce types when possible
|
|
223
|
+
- `removeAdditional: true` - Strip properties not in schema
|
|
224
|
+
|
|
225
|
+
**Note:** `schema.params` is validated at runtime. `schema.returnType` is for documentation/introspection only.
|
|
226
|
+
|
|
227
|
+
## Error Handling
|
|
228
|
+
|
|
229
|
+
### Using ctx.error()
|
|
230
|
+
|
|
231
|
+
The `error()` function is injected into both hooks and handlers:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
Create(
|
|
235
|
+
'GetResource',
|
|
236
|
+
{},
|
|
237
|
+
async (ctx, params) => {
|
|
238
|
+
const resource = await db.find(params.id)
|
|
239
|
+
if (!resource) {
|
|
240
|
+
throw ctx.error(404, 'Resource not found', { id: params.id })
|
|
241
|
+
}
|
|
242
|
+
return resource
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Error Classes
|
|
248
|
+
|
|
249
|
+
| Error Class | Trigger |
|
|
250
|
+
|-------------|---------|
|
|
251
|
+
| ProcedureError | `ctx.error()` in handlers |
|
|
252
|
+
| ProcedureValidationError | Schema validation failure (params) |
|
|
253
|
+
| ProcedureYieldValidationError | Yield validation failure (streaming with `validateYields: true`) |
|
|
254
|
+
| ProcedureRegistrationError | Invalid schema at registration |
|
|
255
|
+
|
|
256
|
+
### Error Properties
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
try {
|
|
260
|
+
await MyProcedure(ctx, params)
|
|
261
|
+
} catch (e) {
|
|
262
|
+
if (e instanceof ProcedureError) {
|
|
263
|
+
console.log(e.procedureName) // 'MyProcedure'
|
|
264
|
+
console.log(e.message) // 'Resource not found'
|
|
265
|
+
console.log(e.meta) // { id: '123' }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Abort Signal
|
|
271
|
+
|
|
272
|
+
For regular procedures, `ctx.signal` is available when the server implementation provides it. The built-in HTTP integrations (Hono RPC, Express RPC) inject the request's abort signal automatically:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
const { Create } = Procedures<{ signal: AbortSignal }>()
|
|
276
|
+
|
|
277
|
+
const { LongQuery } = Create(
|
|
278
|
+
'LongQuery',
|
|
279
|
+
{},
|
|
280
|
+
async (ctx, params) => {
|
|
281
|
+
// Pass signal to downstream operations
|
|
282
|
+
const result = await fetch('https://api.example.com/data', {
|
|
283
|
+
signal: ctx.signal,
|
|
284
|
+
})
|
|
285
|
+
return result.json()
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
When using the Hono or Express implementations, `ctx.signal` aborts when the client disconnects, automatically cancelling in-flight `fetch()` calls, database queries, or any other signal-aware operation.
|
|
291
|
+
|
|
292
|
+
For streaming abort signal behavior (including `signal.reason` and consumer break detection), see [Streaming Procedures](./streaming.md#abort-signal-integration).
|
|
293
|
+
|
|
294
|
+
## Framework Integration
|
|
295
|
+
|
|
296
|
+
### onCreate Callback
|
|
297
|
+
|
|
298
|
+
Register procedures with your framework (Express, Fastify, etc.):
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import express from 'express'
|
|
302
|
+
|
|
303
|
+
const app = express()
|
|
304
|
+
const routes: Map<string, Function> = new Map()
|
|
305
|
+
|
|
306
|
+
const { Create } = Procedures<{ req: Request; res: Response }>({
|
|
307
|
+
onCreate: ({ name, handler, config }) => {
|
|
308
|
+
// Register as Express route
|
|
309
|
+
app.post(`/rpc/${name}`, async (req, res) => {
|
|
310
|
+
try {
|
|
311
|
+
const result = await handler({ req, res }, req.body)
|
|
312
|
+
res.json(result)
|
|
313
|
+
} catch (e) {
|
|
314
|
+
if (e instanceof ProcedureError) {
|
|
315
|
+
res.status(500).json({ error: e.message })
|
|
316
|
+
} else {
|
|
317
|
+
res.status(500).json({ error: 'Internal error' })
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
},
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// Procedures are automatically registered as /rpc/GetUser, /rpc/CreateUser, etc.
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
For built-in HTTP framework integrations (Express RPC, Hono RPC, Hono API, Hono Stream), see [HTTP Integrations](./http-integrations.md).
|
|
328
|
+
|
|
329
|
+
### Introspection with getProcedures()
|
|
330
|
+
|
|
331
|
+
Access all registered procedures for documentation or routing:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
const { Create, getProcedures } = Procedures()
|
|
335
|
+
|
|
336
|
+
Create('GetUser', { schema: { params: Type.Object({ id: Type.String() }) } }, async () => {})
|
|
337
|
+
Create('ListUsers', { schema: { params: Type.Object({}) } }, async () => {})
|
|
338
|
+
|
|
339
|
+
// Get all registered procedures
|
|
340
|
+
const procedures = getProcedures()
|
|
341
|
+
|
|
342
|
+
// Generate OpenAPI spec
|
|
343
|
+
for (const config of procedures) {
|
|
344
|
+
console.log(`${config.name}:`, config.schema)
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Testing
|
|
349
|
+
|
|
350
|
+
Procedures return handlers that can be called directly in tests:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { describe, test, expect } from 'vitest'
|
|
354
|
+
import { Procedures } from 'ts-procedures'
|
|
355
|
+
import { Type } from 'typebox'
|
|
356
|
+
|
|
357
|
+
interface MyCustomContext {
|
|
358
|
+
userId?: string
|
|
359
|
+
userName?: string
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const { Create } = Procedures<MyCustomContext>()
|
|
363
|
+
|
|
364
|
+
const { GetUser, info } = Create(
|
|
365
|
+
'GetUser',
|
|
366
|
+
{
|
|
367
|
+
schema: {
|
|
368
|
+
params: Type.Object({ hideName: Type.Optional(Type.Boolean()) }),
|
|
369
|
+
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
async (ctx, params) => {
|
|
373
|
+
if (!params.userName || !ctx.userId) {
|
|
374
|
+
throw ctx.error('User is not authenticated')
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
id: params.userId,
|
|
379
|
+
name: params?.hideName ? '*******' : params.userName
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
describe('GetUser', () => {
|
|
385
|
+
test('returns user', async () => {
|
|
386
|
+
const result = await GetUser({userId:'123',userName:'Ray'}, { hideName: false })
|
|
387
|
+
expect(result).toEqual({ id: '123', name: 'Ray' })
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
test('hides user name', async () => {
|
|
391
|
+
const result = await GetUser({userId:'123',userName:'Ray'}, { hideName: true })
|
|
392
|
+
expect(result).toEqual({ id: '123', name: '*******' })
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
test('validates params', async () => {
|
|
396
|
+
await expect(GetUser({}, {})).rejects.toThrow(ProcedureValidationError)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
test('has correct schema', () => {
|
|
400
|
+
expect(info.schema.params).toEqual({
|
|
401
|
+
type: 'object',
|
|
402
|
+
properties: { id: { type: 'string' } },
|
|
403
|
+
required: ['id'],
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## API Reference
|
|
410
|
+
|
|
411
|
+
### Procedures(builder?)
|
|
412
|
+
|
|
413
|
+
Creates a procedure factory.
|
|
414
|
+
|
|
415
|
+
**Parameters:**
|
|
416
|
+
- `builder.onCreate` - Callback invoked when each procedure is registered
|
|
417
|
+
|
|
418
|
+
**Returns:**
|
|
419
|
+
- `Create` - Function to define procedures
|
|
420
|
+
- `getProcedures()` - Returns `Array` of all registered procedures
|
|
421
|
+
|
|
422
|
+
### Create(name, config, handler)
|
|
423
|
+
|
|
424
|
+
Defines a procedure.
|
|
425
|
+
|
|
426
|
+
**Parameters:**
|
|
427
|
+
- `name` - Unique procedure name (becomes named export)
|
|
428
|
+
- `config.description` - Optional description
|
|
429
|
+
- `config.schema.params` - Suretype or TypeBox schema for params (validated at runtime)
|
|
430
|
+
- `config.schema.returnType` - Suretype or TypeBox schema for return returnType (documentation only)
|
|
431
|
+
- Additional properties from `TExtendedConfig`
|
|
432
|
+
- `handler` - Async function `(ctx, params) => Promise<returnType>`
|
|
433
|
+
|
|
434
|
+
**Returns:**
|
|
435
|
+
- `{ [name]: handler }` - Named handler export
|
|
436
|
+
- `procedure` - Generic handler reference
|
|
437
|
+
- `info` - Procedure metareturnType
|
|
438
|
+
|
|
439
|
+
### Type Exports
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
import {
|
|
443
|
+
// Core
|
|
444
|
+
Procedures,
|
|
445
|
+
|
|
446
|
+
// Errors
|
|
447
|
+
ProcedureError,
|
|
448
|
+
ProcedureValidationError,
|
|
449
|
+
ProcedureRegistrationError,
|
|
450
|
+
ProcedureYieldValidationError, // For streaming yield validation
|
|
451
|
+
|
|
452
|
+
// Types
|
|
453
|
+
TLocalContext,
|
|
454
|
+
TStreamContext, // Streaming context (AbortSignal always present)
|
|
455
|
+
TProcedureRegistration,
|
|
456
|
+
TStreamProcedureRegistration, // Streaming procedure registration
|
|
457
|
+
TNoContextProvided,
|
|
458
|
+
|
|
459
|
+
// Schema utilities
|
|
460
|
+
extractJsonSchema,
|
|
461
|
+
schemaParser,
|
|
462
|
+
isTypeboxSchema,
|
|
463
|
+
isSuretypeSchema,
|
|
464
|
+
|
|
465
|
+
// Schema types
|
|
466
|
+
TJSONSchema,
|
|
467
|
+
TSchemaLib,
|
|
468
|
+
TSchemaLibGenerator, // AsyncGenerator type utility
|
|
469
|
+
TSchemaParsed,
|
|
470
|
+
TSchemaValidationError,
|
|
471
|
+
Prettify,
|
|
472
|
+
} from 'ts-procedures'
|
|
473
|
+
```
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
[Home](../README.md) | [Core](./core.md) | [Streaming](./streaming.md) | **HTTP Integrations** | [Client & Codegen](./client-and-codegen.md) | [AI Agent Setup](./ai-agent-setup.md)
|
|
2
|
+
|
|
3
|
+
# HTTP Framework Integrations
|
|
4
|
+
|
|
5
|
+
ts-procedures includes built-in HTTP builders for Express and Hono that handle routing, context resolution, lifecycle hooks, abort signals, and automatic route documentation. Each builder uses the `onCreate` callback internally to register procedures as HTTP endpoints.
|
|
6
|
+
|
|
7
|
+
For a full cross-framework comparison (config interfaces, path generation, context resolution, abort signal, lifecycle hooks, route documentation), see the [HTTP implementations overview](../src/implementations/http/README.md).
|
|
8
|
+
|
|
9
|
+
## Integrations at a Glance
|
|
10
|
+
|
|
11
|
+
| Integration | Export | Style | Guide |
|
|
12
|
+
|-------------|--------|-------|-------|
|
|
13
|
+
| Express RPC | `ts-procedures/express-rpc` | RPC (POST routes) | [README](../src/implementations/http/express-rpc/README.md) |
|
|
14
|
+
| Hono RPC | `ts-procedures/hono-rpc` | RPC (POST routes) | [README](../src/implementations/http/hono-rpc/README.md) |
|
|
15
|
+
| Hono Stream | `ts-procedures/hono-stream` | SSE/text streaming | [README](../src/implementations/http/hono-stream/README.md) |
|
|
16
|
+
| Hono API | `ts-procedures/hono-api` | REST (method-based) | [Below](#hono-api-integration) |
|
|
17
|
+
|
|
18
|
+
## Express RPC
|
|
19
|
+
|
|
20
|
+
RPC-style HTTP integration for Express that creates POST routes at `/rpc/{name}/{version}` paths with automatic JSON schema documentation.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { ExpressRPCAppBuilder, RPCConfig } from 'ts-procedures/express-rpc'
|
|
24
|
+
|
|
25
|
+
const RPC = Procedures<AppContext, RPCConfig>()
|
|
26
|
+
|
|
27
|
+
RPC.Create(
|
|
28
|
+
'GetUser',
|
|
29
|
+
{
|
|
30
|
+
name: ['users', 'get'],
|
|
31
|
+
version: 1,
|
|
32
|
+
schema: {
|
|
33
|
+
params: Type.Object({ id: Type.String() }),
|
|
34
|
+
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
async (ctx, params) => {
|
|
38
|
+
return { id: params.id, name: 'John Doe' }
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const app = new ExpressRPCAppBuilder()
|
|
43
|
+
.register(RPC, (req) => ({ userId: req.headers['x-user-id'] as string }))
|
|
44
|
+
.build()
|
|
45
|
+
|
|
46
|
+
app.listen(3000)
|
|
47
|
+
// Route created: POST /rpc/users/get/1
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
See the [Express RPC Integration Guide](../src/implementations/http/express-rpc/README.md) for complete setup including lifecycle hooks, error handling, and route documentation.
|
|
51
|
+
|
|
52
|
+
## Hono RPC
|
|
53
|
+
|
|
54
|
+
RPC-style HTTP integration for Hono with the same routing pattern as Express RPC. Works with Bun, Deno, Cloudflare Workers, and Node.js.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { HonoRPCAppBuilder, RPCConfig } from 'ts-procedures/hono-rpc'
|
|
58
|
+
|
|
59
|
+
const RPC = Procedures<AppContext, RPCConfig>()
|
|
60
|
+
|
|
61
|
+
RPC.Create('GetUser', {
|
|
62
|
+
scope: ['users', 'profile'],
|
|
63
|
+
version: 1,
|
|
64
|
+
schema: { params: Type.Object({ id: Type.String() }) },
|
|
65
|
+
}, async (ctx, params) => {
|
|
66
|
+
return { id: params.id, name: 'John Doe' }
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const app = new HonoRPCAppBuilder({ pathPrefix: '/rpc' })
|
|
70
|
+
.register(RPC, (c) => ({ userId: c.req.header('x-user-id') || 'anonymous' }))
|
|
71
|
+
.build()
|
|
72
|
+
|
|
73
|
+
// POST /rpc/users/profile/get-user/1
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
See the [Hono RPC Integration Guide](../src/implementations/http/hono-rpc/README.md) for complete setup including context resolution, doc extension, abort signal, error handling, and runtime compatibility.
|
|
77
|
+
|
|
78
|
+
## Hono Stream
|
|
79
|
+
|
|
80
|
+
Streaming/SSE integration for Hono that supports both SSE and text streaming modes with yield validation, lifecycle hooks, and client disconnect detection.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { HonoStreamAppBuilder } from 'ts-procedures/hono-stream'
|
|
84
|
+
|
|
85
|
+
const StreamRPC = Procedures<AppContext, RPCConfig>()
|
|
86
|
+
|
|
87
|
+
StreamRPC.CreateStream('WatchNotifications', {
|
|
88
|
+
scope: ['user', 'notifications'],
|
|
89
|
+
version: 1,
|
|
90
|
+
schema: { yieldType: Type.Object({ id: Type.Number(), message: Type.String() }) },
|
|
91
|
+
}, async function* (ctx) {
|
|
92
|
+
for (let i = 1; i <= 10; i++) {
|
|
93
|
+
yield { id: i, message: `Notification ${i}` }
|
|
94
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const app = new HonoStreamAppBuilder()
|
|
99
|
+
.register(StreamRPC, (c) => ({ userId: c.req.header('x-user-id') || 'anonymous' }))
|
|
100
|
+
.build()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
See the [Hono Stream Integration Guide](../src/implementations/http/hono-stream/README.md) for complete setup including SSE/text modes, the `sse()` helper, validation, error handling, and migration notes.
|
|
104
|
+
|
|
105
|
+
## Hono API Integration
|
|
106
|
+
|
|
107
|
+
REST-style HTTP integration for Hono that routes by HTTP method with per-channel input validation via `schema.input`.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import { Procedures } from 'ts-procedures'
|
|
111
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
112
|
+
import type { APIConfig } from 'ts-procedures/http'
|
|
113
|
+
import { Type } from 'typebox'
|
|
114
|
+
|
|
115
|
+
const API = Procedures<{ userId: string }, APIConfig>()
|
|
116
|
+
|
|
117
|
+
API.Create('GetUser', {
|
|
118
|
+
path: '/users/:id',
|
|
119
|
+
method: 'get',
|
|
120
|
+
schema: {
|
|
121
|
+
input: {
|
|
122
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
123
|
+
},
|
|
124
|
+
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
125
|
+
},
|
|
126
|
+
}, async (ctx, { pathParams }) => {
|
|
127
|
+
return await fetchUser(pathParams.id)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
API.Create('CreateUser', {
|
|
131
|
+
path: '/users',
|
|
132
|
+
method: 'post',
|
|
133
|
+
schema: {
|
|
134
|
+
input: {
|
|
135
|
+
body: Type.Object({ name: Type.String(), email: Type.String() }),
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
}, async (ctx, { body }) => {
|
|
139
|
+
return await createUser(body)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const app = await new HonoAPIAppBuilder({ pathPrefix: '/api' })
|
|
143
|
+
.register(API, (c) => ({ userId: c.req.header('x-user-id') || 'anonymous' }))
|
|
144
|
+
.build()
|
|
145
|
+
|
|
146
|
+
// Routes:
|
|
147
|
+
// GET /api/users/:id -> 200
|
|
148
|
+
// POST /api/users -> 201
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## DocRegistry — Composing Docs from Multiple Builders
|
|
152
|
+
|
|
153
|
+
Use `DocRegistry` to compose route documentation from any combination of HTTP builders into a typed envelope:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { DocRegistry } from 'ts-procedures/http-docs'
|
|
157
|
+
|
|
158
|
+
const docs = new DocRegistry({
|
|
159
|
+
basePath: '/api',
|
|
160
|
+
headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
|
|
161
|
+
errors: DocRegistry.defaultErrors(),
|
|
162
|
+
})
|
|
163
|
+
.from(rpcBuilder)
|
|
164
|
+
.from(apiBuilder)
|
|
165
|
+
.from(streamBuilder)
|
|
166
|
+
|
|
167
|
+
app.get('/docs', (c) => c.json(docs.toJSON()))
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`from()` stores a reference — routes are read lazily at `toJSON()` time, so builders can be registered before or after `.build()`. Supports optional `filter` and `transform` options for customizing output.
|
|
171
|
+
|
|
172
|
+
The `DocRegistry` output is the input for [Client Code Generation](./client-and-codegen.md).
|
|
173
|
+
|
|
174
|
+
## Type Exports
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// HTTP types
|
|
178
|
+
import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode, APIConfig, APIHttpRouteDoc, APIInput, HttpMethod } from 'ts-procedures/http'
|
|
179
|
+
|
|
180
|
+
// Hono API (REST-style)
|
|
181
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
182
|
+
import type { APIConfig, APIHttpRouteDoc, APIInput, HttpMethod, QueryParser } from 'ts-procedures/hono-api'
|
|
183
|
+
```
|