ts-procedures 5.3.0 → 5.4.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 +90 -0
- package/agent_config/claude-code/agents/ts-procedures-architect.md +15 -0
- package/agent_config/claude-code/skills/guide/anti-patterns.md +106 -0
- package/agent_config/claude-code/skills/guide/api-reference.md +150 -4
- package/agent_config/claude-code/skills/guide/patterns.md +155 -0
- package/agent_config/claude-code/skills/review/checklist.md +22 -0
- package/agent_config/claude-code/skills/scaffold/SKILL.md +3 -1
- package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +169 -0
- package/agent_config/copilot/copilot-instructions.md +35 -0
- package/agent_config/cursor/cursorrules +35 -0
- package/build/implementations/http/hono-api/index.d.ts +102 -0
- package/build/implementations/http/hono-api/index.js +339 -0
- package/build/implementations/http/hono-api/index.js.map +1 -0
- package/build/implementations/http/hono-api/index.test.d.ts +1 -0
- package/build/implementations/http/hono-api/index.test.js +983 -0
- package/build/implementations/http/hono-api/index.test.js.map +1 -0
- package/build/implementations/http/hono-api/types.d.ts +13 -0
- package/build/implementations/http/hono-api/types.js +2 -0
- package/build/implementations/http/hono-api/types.js.map +1 -0
- package/build/implementations/types.d.ts +44 -0
- package/build/index.d.ts +28 -6
- package/build/index.js +28 -0
- package/build/index.js.map +1 -1
- package/build/schema/compute-schema.d.ts +5 -0
- package/build/schema/compute-schema.js +8 -1
- package/build/schema/compute-schema.js.map +1 -1
- package/build/schema/parser.d.ts +6 -5
- package/build/schema/parser.js +54 -0
- package/build/schema/parser.js.map +1 -1
- package/package.json +8 -3
- package/src/implementations/http/README.md +45 -2
- package/src/implementations/http/hono-api/index.test.ts +1328 -0
- package/src/implementations/http/hono-api/index.ts +461 -0
- package/src/implementations/http/hono-api/types.ts +16 -0
- package/src/implementations/types.ts +52 -0
- package/src/index.ts +87 -10
- package/src/schema/compute-schema.ts +23 -2
- package/src/schema/parser.ts +70 -3
package/README.md
CHANGED
|
@@ -71,6 +71,41 @@ Create(name, config, handler)
|
|
|
71
71
|
- `procedure` - Generic reference to the handler
|
|
72
72
|
- `info` - Procedure meta (name, description, schema, `TExtendedConfig` properties, etc.)
|
|
73
73
|
|
|
74
|
+
### Structured Input with schema.input
|
|
75
|
+
|
|
76
|
+
For HTTP APIs and other multi-channel transports, `schema.input` provides per-channel type safety. Each key is an independently validated input channel:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const { Create } = Procedures<AppContext, APIConfig>()
|
|
80
|
+
|
|
81
|
+
const { UpdateUser } = Create(
|
|
82
|
+
'UpdateUser',
|
|
83
|
+
{
|
|
84
|
+
path: '/users/:id',
|
|
85
|
+
method: 'put',
|
|
86
|
+
schema: {
|
|
87
|
+
input: {
|
|
88
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
89
|
+
query: Type.Object({ notify: Type.Optional(Type.Boolean()) }),
|
|
90
|
+
body: Type.Object({ name: Type.String(), email: Type.String() }),
|
|
91
|
+
},
|
|
92
|
+
returnType: Type.Object({ ok: Type.Boolean() }),
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
async (ctx, { pathParams, query, body }) => {
|
|
96
|
+
// Each channel is independently typed and validated
|
|
97
|
+
await updateUser(pathParams.id, body)
|
|
98
|
+
if (query.notify) await sendNotification(pathParams.id)
|
|
99
|
+
return { ok: true }
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Rules:**
|
|
105
|
+
- `schema.input` and `schema.params` are **mutually exclusive** — defining both throws `ProcedureRegistrationError`
|
|
106
|
+
- Each channel is validated independently with per-channel error messages
|
|
107
|
+
- Works with both `Create` and `CreateStream`
|
|
108
|
+
|
|
74
109
|
### CreateStream Function
|
|
75
110
|
|
|
76
111
|
The `CreateStream` function defines streaming procedures that yield values over time using async generators:
|
|
@@ -625,6 +660,54 @@ app.listen(3000)
|
|
|
625
660
|
|
|
626
661
|
See [Express RPC Integration Guide](src/implementations/http/express-rpc/README.md) for complete setup instructions including lifecycle hooks, error handling, and route documentation.
|
|
627
662
|
|
|
663
|
+
### Hono API Integration
|
|
664
|
+
|
|
665
|
+
`ts-procedures` includes a REST-style HTTP integration for Hono that routes by HTTP method with per-channel input validation via `schema.input`.
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
import { Procedures } from 'ts-procedures'
|
|
669
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
670
|
+
import type { APIConfig } from 'ts-procedures/http'
|
|
671
|
+
import { Type } from 'typebox'
|
|
672
|
+
|
|
673
|
+
const API = Procedures<{ userId: string }, APIConfig>()
|
|
674
|
+
|
|
675
|
+
API.Create('GetUser', {
|
|
676
|
+
path: '/users/:id',
|
|
677
|
+
method: 'get',
|
|
678
|
+
schema: {
|
|
679
|
+
input: {
|
|
680
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
681
|
+
},
|
|
682
|
+
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
683
|
+
},
|
|
684
|
+
}, async (ctx, { pathParams }) => {
|
|
685
|
+
return await fetchUser(pathParams.id)
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
API.Create('CreateUser', {
|
|
689
|
+
path: '/users',
|
|
690
|
+
method: 'post',
|
|
691
|
+
schema: {
|
|
692
|
+
input: {
|
|
693
|
+
body: Type.Object({ name: Type.String(), email: Type.String() }),
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
}, async (ctx, { body }) => {
|
|
697
|
+
return await createUser(body)
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
const app = await new HonoAPIAppBuilder({ pathPrefix: '/api' })
|
|
701
|
+
.register(API, (c) => ({ userId: c.req.header('x-user-id') || 'anonymous' }))
|
|
702
|
+
.build()
|
|
703
|
+
|
|
704
|
+
// Routes:
|
|
705
|
+
// GET /api/users/:id → 200
|
|
706
|
+
// POST /api/users → 201
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
See [Hono API Integration Guide](src/implementations/http/hono-api/) for complete setup.
|
|
710
|
+
|
|
628
711
|
### Introspection with getProcedures()
|
|
629
712
|
|
|
630
713
|
Access all registered procedures for documentation or routing:
|
|
@@ -769,6 +852,13 @@ import {
|
|
|
769
852
|
TSchemaValidationError,
|
|
770
853
|
Prettify,
|
|
771
854
|
} from 'ts-procedures'
|
|
855
|
+
|
|
856
|
+
// HTTP types
|
|
857
|
+
import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode, APIConfig, APIHttpRouteDoc, APIInput, HttpMethod } from 'ts-procedures/http'
|
|
858
|
+
|
|
859
|
+
// Hono API (REST-style)
|
|
860
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
861
|
+
import type { APIConfig, APIHttpRouteDoc, APIInput, HttpMethod, QueryParser } from 'ts-procedures/hono-api'
|
|
772
862
|
```
|
|
773
863
|
|
|
774
864
|
## AI Agent Setup
|
|
@@ -16,6 +16,7 @@ You are an architecture planning agent for applications built with **ts-procedur
|
|
|
16
16
|
| `TContext` | Base context type injected into all handlers (auth, request info, services) |
|
|
17
17
|
| `TExtendedConfig` | Additional config fields on every procedure (scope, version, permissions, etc.) |
|
|
18
18
|
| `schema.params` | TypeBox schema — validated at runtime via AJV |
|
|
19
|
+
| `schema.input` | Structured multi-channel input — each key independently typed/validated (alternative to `schema.params`) |
|
|
19
20
|
| `schema.returnType` | Documentation only — NOT validated at runtime |
|
|
20
21
|
| `schema.yieldType` | Schema for each streamed value — validated only if `validateYields: true` |
|
|
21
22
|
|
|
@@ -42,6 +43,7 @@ You are an architecture planning agent for applications built with **ts-procedur
|
|
|
42
43
|
| `ExpressRPCAppBuilder` | Existing Express app, need RPC endpoints alongside REST |
|
|
43
44
|
| `HonoRPCAppBuilder` | Existing Hono app, edge/serverless, need RPC endpoints |
|
|
44
45
|
| `HonoStreamAppBuilder` | Need streaming (SSE or text), Hono-based |
|
|
46
|
+
| `HonoAPIAppBuilder` | REST-style API with per-channel input (pathParams, query, body, headers) |
|
|
45
47
|
| Multiple builders | Combine RPC + streaming on the same Hono app |
|
|
46
48
|
|
|
47
49
|
### Stream mode?
|
|
@@ -49,6 +51,13 @@ You are an architecture planning agent for applications built with **ts-procedur
|
|
|
49
51
|
- **SSE** (`'sse'`) — Browser EventSource API, automatic reconnection, event types. Default.
|
|
50
52
|
- **Text** (`'text'`) — Newline-delimited JSON. Simpler, works with any HTTP client.
|
|
51
53
|
|
|
54
|
+
### schema.params vs schema.input?
|
|
55
|
+
|
|
56
|
+
- **RPC-style** (single `params` object, POST-only) → `schema.params`
|
|
57
|
+
- **REST-style** (multiple HTTP input sources) → `schema.input`
|
|
58
|
+
- `schema.input` and `schema.params` are **mutually exclusive**
|
|
59
|
+
- `schema.input` channels: `pathParams`, `query`, `body`, `headers`
|
|
60
|
+
|
|
52
61
|
### How to structure context?
|
|
53
62
|
|
|
54
63
|
```typescript
|
|
@@ -119,6 +128,9 @@ When asked to plan an API or procedure set:
|
|
|
119
128
|
- `onCreate` callback on the factory enables framework integration — use it for route registration, middleware setup, or documentation generation.
|
|
120
129
|
- `getProcedures()` returns all registered procedures for introspection (OpenAPI generation, testing, etc.).
|
|
121
130
|
- AJV is configured with `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`.
|
|
131
|
+
- `schema.params` and `schema.input` are mutually exclusive — defining both throws `ProcedureRegistrationError`.
|
|
132
|
+
- `HonoAPIAppBuilder.build()` is async — always `await` it.
|
|
133
|
+
- Path param names in route template (`:id`) must match `schema.input.pathParams` property names.
|
|
122
134
|
|
|
123
135
|
## Output Format
|
|
124
136
|
|
|
@@ -164,6 +176,9 @@ type AuthContext = { userId: string; requestId: string; db: Database }
|
|
|
164
176
|
- POST /api/health/health-check/1
|
|
165
177
|
- POST /api/users/get-user/1
|
|
166
178
|
- GET|POST /api/activity/stream-activity/1
|
|
179
|
+
- GET /api/users/:id (HonoAPIAppBuilder)
|
|
180
|
+
- POST /api/users (HonoAPIAppBuilder, 201)
|
|
181
|
+
- DELETE /api/users/:id (HonoAPIAppBuilder, 204)
|
|
167
182
|
|
|
168
183
|
### Error Handling
|
|
169
184
|
- Input validation: automatic (schema.params)
|
|
@@ -480,6 +480,109 @@ new ExpressRPCAppBuilder({
|
|
|
480
480
|
|
|
481
481
|
---
|
|
482
482
|
|
|
483
|
+
## 17. Using Both schema.params and schema.input
|
|
484
|
+
|
|
485
|
+
**Problem:** Defining both `schema.params` and `schema.input` on the same procedure.
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
// BAD — throws ProcedureRegistrationError at registration
|
|
489
|
+
Create('GetUser', {
|
|
490
|
+
path: '/users/:id',
|
|
491
|
+
method: 'get',
|
|
492
|
+
schema: {
|
|
493
|
+
params: Type.Object({ id: Type.String() }),
|
|
494
|
+
input: {
|
|
495
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
}, handler)
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**Fix:** Choose one. Use `schema.params` for flat RPC-style input, `schema.input` for structured multi-channel input.
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
// GOOD — schema.input for REST-style with per-channel validation
|
|
505
|
+
Create('GetUser', {
|
|
506
|
+
path: '/users/:id',
|
|
507
|
+
method: 'get',
|
|
508
|
+
schema: {
|
|
509
|
+
input: {
|
|
510
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
}, async (ctx, { pathParams }) => fetchUser(pathParams.id))
|
|
514
|
+
|
|
515
|
+
// GOOD — schema.params for simple RPC-style
|
|
516
|
+
Create('GetUser', {
|
|
517
|
+
scope: 'users', version: 1,
|
|
518
|
+
schema: { params: Type.Object({ id: Type.String() }) },
|
|
519
|
+
}, async (ctx, params) => fetchUser(params.id))
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**Why:** `schema.params` and `schema.input` are mutually exclusive by design. They represent different input paradigms — flat (RPC) vs structured (HTTP API).
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## 18. Mismatched Path Param Names in schema.input
|
|
527
|
+
|
|
528
|
+
**Problem:** Path template param names don't match `schema.input.pathParams` property names.
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
// BAD — path has :id but schema declares userId — throws at build time
|
|
532
|
+
Create('GetUser', {
|
|
533
|
+
path: '/users/:id',
|
|
534
|
+
method: 'get',
|
|
535
|
+
schema: {
|
|
536
|
+
input: {
|
|
537
|
+
pathParams: Type.Object({ userId: Type.String() }), // Wrong name!
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
}, handler)
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**Fix:** Ensure path param names match schema property names exactly.
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
// GOOD
|
|
547
|
+
Create('GetUser', {
|
|
548
|
+
path: '/users/:id',
|
|
549
|
+
method: 'get',
|
|
550
|
+
schema: {
|
|
551
|
+
input: {
|
|
552
|
+
pathParams: Type.Object({ id: Type.String() }), // Matches :id
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
}, handler)
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**Why:** `HonoAPIAppBuilder` validates at build time that `:param` names in the path match schema properties. A mismatch would cause runtime validation failures with confusing error messages.
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## 19. Forgetting build() is Async on HonoAPIAppBuilder
|
|
563
|
+
|
|
564
|
+
**Problem:** Not awaiting `build()` on `HonoAPIAppBuilder`.
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
// BAD — build() returns Promise<Hono>, not Hono
|
|
568
|
+
const app = new HonoAPIAppBuilder()
|
|
569
|
+
.register(API, ctx)
|
|
570
|
+
.build() // This is a Promise!
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**Fix:** Await the build call.
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
// GOOD
|
|
577
|
+
const app = await new HonoAPIAppBuilder()
|
|
578
|
+
.register(API, ctx)
|
|
579
|
+
.build()
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
**Why:** `HonoAPIAppBuilder.build()` is async because it resolves the query parser (lazy-loads `qs` optional peer dependency) once at build time for synchronous per-request usage.
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
483
586
|
## Summary Table
|
|
484
587
|
|
|
485
588
|
| # | Anti-Pattern | Risk | Severity |
|
|
@@ -500,3 +603,6 @@ new ExpressRPCAppBuilder({
|
|
|
500
603
|
| 14 | Wrong error handler for streams | Unhandled errors or wrong response format | WARNING |
|
|
501
604
|
| 15 | Manual doc building | Fragile, incomplete documentation | SUGGESTION |
|
|
502
605
|
| 16 | Unhandled async context factory | Request crashes | WARNING |
|
|
606
|
+
| 17 | Both schema.params and schema.input | ProcedureRegistrationError at startup | CRITICAL |
|
|
607
|
+
| 18 | Mismatched path param names | Build-time error or confusing validation failures | CRITICAL |
|
|
608
|
+
| 19 | Not awaiting HonoAPIAppBuilder.build() | Using unresolved Promise as app | CRITICAL |
|
|
@@ -31,7 +31,7 @@ import { captureDefinitionInfo, formatDefinitionInfo } from 'ts-procedures'
|
|
|
31
31
|
import type { DefinitionLocation, DefinitionInfo } from 'ts-procedures'
|
|
32
32
|
|
|
33
33
|
// HTTP types (types only, no runtime)
|
|
34
|
-
import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode } from 'ts-procedures/http'
|
|
34
|
+
import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode, APIConfig, APIHttpRouteDoc, APIInput, HttpMethod } from 'ts-procedures/http'
|
|
35
35
|
|
|
36
36
|
// Express RPC
|
|
37
37
|
import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
|
|
@@ -41,6 +41,10 @@ import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
|
|
|
41
41
|
|
|
42
42
|
// Hono Streaming
|
|
43
43
|
import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
|
|
44
|
+
|
|
45
|
+
// Hono API (REST-style)
|
|
46
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
47
|
+
import type { APIConfig, APIHttpRouteDoc, APIInput, HttpMethod } from 'ts-procedures/hono-api'
|
|
44
48
|
```
|
|
45
49
|
|
|
46
50
|
---
|
|
@@ -144,6 +148,31 @@ function Create<TName extends string, TParams, TReturnType>(
|
|
|
144
148
|
- `{ procedure: handler }` — Same handler under a fixed key
|
|
145
149
|
- `{ info: metadata }` — Registration metadata with computed JSON schemas and validation functions
|
|
146
150
|
|
|
151
|
+
### schema.input (Structured Multi-Channel Input)
|
|
152
|
+
|
|
153
|
+
`schema.input` is an alternative to `schema.params` for multi-channel input. Mutually exclusive with `schema.params`.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
Create('UpdateUser', {
|
|
157
|
+
path: '/users/:id',
|
|
158
|
+
method: 'put',
|
|
159
|
+
schema: {
|
|
160
|
+
input: {
|
|
161
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
162
|
+
body: Type.Object({ name: Type.String() }),
|
|
163
|
+
},
|
|
164
|
+
returnType: Type.Object({ ok: Type.Boolean() }),
|
|
165
|
+
},
|
|
166
|
+
}, async (ctx, { pathParams, body }) => {
|
|
167
|
+
// Each channel independently typed via TInput generic
|
|
168
|
+
return { ok: true }
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- Each key in `input` is independently validated with per-channel error messages
|
|
173
|
+
- `ProcedureRegistrationError` thrown at registration if both `params` and `input` defined
|
|
174
|
+
- `TInput` generic inferred from `schema.input` — handler params type computed as `{ [K in keyof TInput]: TSchemaLib<TInput[K]> }`
|
|
175
|
+
|
|
147
176
|
### Throws
|
|
148
177
|
|
|
149
178
|
- `ProcedureRegistrationError` — If schema extraction/compilation fails, or duplicate name
|
|
@@ -266,7 +295,7 @@ class ProcedureYieldValidationError extends ProcedureError {
|
|
|
266
295
|
|
|
267
296
|
### ProcedureRegistrationError extends ProcedureError
|
|
268
297
|
|
|
269
|
-
Thrown at registration time (schema extraction/compilation failure, duplicate name).
|
|
298
|
+
Thrown at registration time (schema extraction/compilation failure, duplicate name, or `schema.params` and `schema.input` both defined).
|
|
270
299
|
|
|
271
300
|
```typescript
|
|
272
301
|
class ProcedureRegistrationError extends ProcedureError {
|
|
@@ -295,8 +324,8 @@ Compiles schemas into AJV validator functions.
|
|
|
295
324
|
|
|
296
325
|
```typescript
|
|
297
326
|
function schemaParser(
|
|
298
|
-
schema: { params?: unknown; returnType?: unknown; yieldType?: unknown },
|
|
299
|
-
onParseError: (errors:
|
|
327
|
+
schema: { params?: unknown; returnType?: unknown; yieldType?: unknown; input?: Record<string, unknown> },
|
|
328
|
+
onParseError: (errors: Record<string, string>) => void
|
|
300
329
|
): TSchemaParsed
|
|
301
330
|
```
|
|
302
331
|
|
|
@@ -476,6 +505,78 @@ type MidStreamErrorResult<TErrorData = unknown> = {
|
|
|
476
505
|
|
|
477
506
|
---
|
|
478
507
|
|
|
508
|
+
## HonoAPIAppBuilder
|
|
509
|
+
|
|
510
|
+
REST-style HTTP implementation for Hono. Routes by HTTP method with `schema.input` per-channel validation.
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
class HonoAPIAppBuilder {
|
|
514
|
+
constructor(config?: {
|
|
515
|
+
app?: Hono
|
|
516
|
+
pathPrefix?: string
|
|
517
|
+
queryParser?: (queryString: string) => Record<string, unknown>
|
|
518
|
+
onRequestStart?: (c: Context) => void
|
|
519
|
+
onRequestEnd?: (c: Context) => void
|
|
520
|
+
onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
|
|
521
|
+
onError?: (procedure: TProcedureRegistration, c: Context, error: Error) => Response | Promise<Response>
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
register<TFactory>(
|
|
525
|
+
factory: TFactory,
|
|
526
|
+
factoryContext: Context | ((c: HonoContext) => Context | Promise<Context>),
|
|
527
|
+
extendProcedureDoc?: ({ base, procedure }) => Record<string, any>
|
|
528
|
+
): this
|
|
529
|
+
|
|
530
|
+
async build(): Promise<Hono>
|
|
531
|
+
get app(): Hono
|
|
532
|
+
get docs(): APIHttpRouteDoc[]
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Route Path
|
|
537
|
+
|
|
538
|
+
Developer-defined via `APIConfig.path` — no auto-generation. Supports Hono path params (`:id`).
|
|
539
|
+
|
|
540
|
+
`{pathPrefix}{path}` — e.g., `/api/users/:id`
|
|
541
|
+
|
|
542
|
+
### Input Channel Extraction
|
|
543
|
+
|
|
544
|
+
| Channel | HTTP Source | Used For |
|
|
545
|
+
|---------|-----------|----------|
|
|
546
|
+
| `pathParams` | URL path parameters (`c.req.param()`) | `/users/:id` → `{ id: '...' }` |
|
|
547
|
+
| `query` | Query string (parsed via `qs` or native) | `?page=2` → `{ page: 2 }` |
|
|
548
|
+
| `body` | JSON request body (POST/PUT/PATCH only) | `{ name: 'John' }` |
|
|
549
|
+
| `headers` | Request headers (AJV strips non-declared) | `{ 'x-api-key': '...' }` |
|
|
550
|
+
|
|
551
|
+
### Default Success Status
|
|
552
|
+
|
|
553
|
+
| Method | Default Status |
|
|
554
|
+
|--------|---------------|
|
|
555
|
+
| POST | 201 |
|
|
556
|
+
| DELETE | 204 (no body) |
|
|
557
|
+
| Others | 200 |
|
|
558
|
+
|
|
559
|
+
Override with `APIConfig.successStatus`.
|
|
560
|
+
|
|
561
|
+
### Build-Time Validation
|
|
562
|
+
|
|
563
|
+
- Path param names in path template must match `schema.input.pathParams` property names
|
|
564
|
+
- `schema.input.pathParams` defined but path has no `:param` segments → error
|
|
565
|
+
- Path has `:param` but `schema.input.pathParams` missing → error
|
|
566
|
+
- Property name mismatch → error with clear message
|
|
567
|
+
|
|
568
|
+
### Query Parsing
|
|
569
|
+
|
|
570
|
+
Priority: custom `queryParser` > `qs` (optional peer dep, lazy-loaded) > native `URLSearchParams`.
|
|
571
|
+
|
|
572
|
+
Query parser resolved once at `build()` time — handlers use it synchronously.
|
|
573
|
+
|
|
574
|
+
### Signal Injection
|
|
575
|
+
|
|
576
|
+
Uses `c.req.raw.signal` — native Request signal from Hono context.
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
479
580
|
## HTTP Types
|
|
480
581
|
|
|
481
582
|
### RPCConfig
|
|
@@ -517,6 +618,51 @@ interface StreamHttpRouteDoc extends RPCConfig {
|
|
|
517
618
|
}
|
|
518
619
|
```
|
|
519
620
|
|
|
621
|
+
### APIConfig
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
624
|
+
interface APIConfig {
|
|
625
|
+
path: string
|
|
626
|
+
method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'
|
|
627
|
+
successStatus?: number
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### APIHttpRouteDoc
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
interface APIHttpRouteDoc extends APIConfig {
|
|
635
|
+
name: string
|
|
636
|
+
fullPath: string
|
|
637
|
+
jsonSchema: {
|
|
638
|
+
pathParams?: Record<string, unknown>
|
|
639
|
+
query?: Record<string, unknown>
|
|
640
|
+
body?: Record<string, unknown>
|
|
641
|
+
headers?: Record<string, unknown>
|
|
642
|
+
response?: Record<string, unknown>
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### APIInput
|
|
648
|
+
|
|
649
|
+
Channel constraint helper type. Use with `satisfies` for compile-time validation of channel names.
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
type APIInput<T extends {
|
|
653
|
+
pathParams?: unknown
|
|
654
|
+
query?: unknown
|
|
655
|
+
body?: unknown
|
|
656
|
+
headers?: unknown
|
|
657
|
+
}> = T
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### HttpMethod
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'
|
|
664
|
+
```
|
|
665
|
+
|
|
520
666
|
---
|
|
521
667
|
|
|
522
668
|
## Stack Utilities
|
|
@@ -387,6 +387,161 @@ const app = new HonoStreamAppBuilder({
|
|
|
387
387
|
|
|
388
388
|
---
|
|
389
389
|
|
|
390
|
+
## Hono API Integration (REST-style)
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
import { Procedures } from 'ts-procedures'
|
|
394
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
395
|
+
import type { APIConfig } from 'ts-procedures/http'
|
|
396
|
+
import { Type } from 'typebox'
|
|
397
|
+
|
|
398
|
+
type AppContext = { userId: string }
|
|
399
|
+
const API = Procedures<AppContext, APIConfig>()
|
|
400
|
+
|
|
401
|
+
API.Create('GetUser', {
|
|
402
|
+
path: '/users/:id',
|
|
403
|
+
method: 'get',
|
|
404
|
+
schema: {
|
|
405
|
+
input: {
|
|
406
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
407
|
+
},
|
|
408
|
+
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
409
|
+
},
|
|
410
|
+
}, async (ctx, { pathParams }) => {
|
|
411
|
+
return await fetchUser(pathParams.id)
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
API.Create('CreateUser', {
|
|
415
|
+
path: '/users',
|
|
416
|
+
method: 'post',
|
|
417
|
+
schema: {
|
|
418
|
+
input: {
|
|
419
|
+
body: Type.Object({ name: Type.String(), email: Type.String() }),
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
}, async (ctx, { body }) => {
|
|
423
|
+
return await createUser(body)
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
API.Create('DeleteUser', {
|
|
427
|
+
path: '/users/:id',
|
|
428
|
+
method: 'delete',
|
|
429
|
+
schema: {
|
|
430
|
+
input: {
|
|
431
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
}, async (ctx, { pathParams }) => {
|
|
435
|
+
await deleteUser(pathParams.id)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
const app = await new HonoAPIAppBuilder({
|
|
439
|
+
pathPrefix: '/api',
|
|
440
|
+
onError: (procedure, c, error) => {
|
|
441
|
+
if (error instanceof ProcedureValidationError) {
|
|
442
|
+
return c.json({ error: error.message, details: error.errors }, 400)
|
|
443
|
+
}
|
|
444
|
+
return c.json({ error: error.message }, 500)
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
.register(API, (c) => ({
|
|
448
|
+
userId: c.req.header('x-user-id') || 'anonymous',
|
|
449
|
+
}))
|
|
450
|
+
.build()
|
|
451
|
+
|
|
452
|
+
// Routes:
|
|
453
|
+
// GET /api/users/:id → 200
|
|
454
|
+
// POST /api/users → 201
|
|
455
|
+
// DELETE /api/users/:id → 204
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Extended Config with APIConfig
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
import { Procedures } from 'ts-procedures'
|
|
464
|
+
import type { APIConfig } from 'ts-procedures/http'
|
|
465
|
+
|
|
466
|
+
type AppContext = { userId: string }
|
|
467
|
+
|
|
468
|
+
const { Create } = Procedures<AppContext, APIConfig>()
|
|
469
|
+
|
|
470
|
+
// Every procedure now MUST include path and method
|
|
471
|
+
const { GetUser } = Create(
|
|
472
|
+
'GetUser',
|
|
473
|
+
{
|
|
474
|
+
path: '/users/:id',
|
|
475
|
+
method: 'get',
|
|
476
|
+
schema: {
|
|
477
|
+
input: {
|
|
478
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
479
|
+
query: Type.Object({ include: Type.Optional(Type.String()) }),
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
async (ctx, { pathParams, query }) => {
|
|
484
|
+
return await fetchUser(pathParams.id, { include: query.include })
|
|
485
|
+
}
|
|
486
|
+
)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## schema.input — Multi-Channel Structured Input
|
|
492
|
+
|
|
493
|
+
Use `schema.input` instead of `schema.params` for structured per-channel validation:
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
const { Create } = Procedures<AppContext, APIConfig>()
|
|
497
|
+
|
|
498
|
+
Create('UpdateUserField', {
|
|
499
|
+
path: '/users/:id',
|
|
500
|
+
method: 'put',
|
|
501
|
+
schema: {
|
|
502
|
+
input: {
|
|
503
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
504
|
+
query: Type.Object({ notify: Type.Optional(Type.Boolean()) }),
|
|
505
|
+
body: Type.Object({ field: Type.String(), value: Type.String() }),
|
|
506
|
+
headers: Type.Object({ 'x-idempotency-key': Type.String() }),
|
|
507
|
+
},
|
|
508
|
+
returnType: Type.Object({ ok: Type.Boolean() }),
|
|
509
|
+
},
|
|
510
|
+
}, async (ctx, { pathParams, query, body, headers }) => {
|
|
511
|
+
// Each channel independently typed and validated
|
|
512
|
+
// Validation errors include channel name: "Validation error for UpdateUserField in input.body"
|
|
513
|
+
await updateField(pathParams.id, body.field, body.value, {
|
|
514
|
+
idempotencyKey: headers['x-idempotency-key'],
|
|
515
|
+
})
|
|
516
|
+
if (query.notify) await notifyUser(pathParams.id)
|
|
517
|
+
return { ok: true }
|
|
518
|
+
})
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## APIInput Channel Constraint
|
|
524
|
+
|
|
525
|
+
Use `satisfies APIInput` to catch channel name typos at compile time:
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
import type { APIInput } from 'ts-procedures/hono-api'
|
|
529
|
+
|
|
530
|
+
Create('Search', {
|
|
531
|
+
path: '/search',
|
|
532
|
+
method: 'get',
|
|
533
|
+
schema: {
|
|
534
|
+
input: {
|
|
535
|
+
query: Type.Object({ q: Type.String() }),
|
|
536
|
+
} satisfies APIInput,
|
|
537
|
+
},
|
|
538
|
+
}, async (ctx, { query }) => {
|
|
539
|
+
return await search(query.q)
|
|
540
|
+
})
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
390
545
|
## onCreate Callback for Framework Integration
|
|
391
546
|
|
|
392
547
|
```typescript
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
### CRITICAL
|
|
29
29
|
- [ ] Uses TypeBox (`Type.Object(...)`, `Type.String()`, etc.) — not plain objects
|
|
30
30
|
- [ ] Required fields use `Type.String()` directly; optional fields wrapped with `Type.Optional(...)`
|
|
31
|
+
- [ ] Does not define both `schema.params` and `schema.input` on the same procedure (mutually exclusive)
|
|
31
32
|
|
|
32
33
|
### WARNING
|
|
33
34
|
- [ ] Understands AJV `coerceTypes: true` — no manual type parsing for query string values
|
|
@@ -93,6 +94,27 @@
|
|
|
93
94
|
|
|
94
95
|
---
|
|
95
96
|
|
|
97
|
+
## API Builder Checks (HonoAPIAppBuilder)
|
|
98
|
+
|
|
99
|
+
### CRITICAL
|
|
100
|
+
- [ ] `build()` is `await`ed — returns `Promise<Hono>`, not `Hono`
|
|
101
|
+
- [ ] Path param names in route template match `schema.input.pathParams` property names exactly
|
|
102
|
+
- [ ] Does not define both `schema.params` and `schema.input` on the same procedure
|
|
103
|
+
- [ ] `onError` callback handles `ProcedureValidationError` and `ProcedureError` differently
|
|
104
|
+
|
|
105
|
+
### WARNING
|
|
106
|
+
- [ ] Uses `schema.input` channels appropriate for the HTTP method (no `body` on GET/HEAD)
|
|
107
|
+
- [ ] `pathPrefix` set consistently
|
|
108
|
+
- [ ] `successStatus` overridden only when default (POST→201, DELETE→204) is wrong
|
|
109
|
+
- [ ] Uses `APIInput` type constraint (`satisfies APIInput`) to catch channel name typos
|
|
110
|
+
|
|
111
|
+
### SUGGESTION
|
|
112
|
+
- [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
|
|
113
|
+
- [ ] Custom `queryParser` provided if complex query string formats needed
|
|
114
|
+
- [ ] Lifecycle hooks used for observability (logging, metrics)
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
96
118
|
## Error Handling Checks
|
|
97
119
|
|
|
98
120
|
### CRITICAL
|
|
@@ -7,7 +7,7 @@ invocable_by:
|
|
|
7
7
|
user_instructions: |
|
|
8
8
|
Usage: /ts-procedures:scaffold <type> <Name>
|
|
9
9
|
|
|
10
|
-
Types: procedure, stream-procedure, express-rpc, hono-rpc, hono-stream
|
|
10
|
+
Types: procedure, stream-procedure, express-rpc, hono-rpc, hono-stream, hono-api
|
|
11
11
|
|
|
12
12
|
Examples:
|
|
13
13
|
/ts-procedures:scaffold procedure GetUser
|
|
@@ -15,6 +15,7 @@ user_instructions: |
|
|
|
15
15
|
/ts-procedures:scaffold express-rpc UserApi
|
|
16
16
|
/ts-procedures:scaffold hono-rpc OrderApi
|
|
17
17
|
/ts-procedures:scaffold hono-stream LiveFeed
|
|
18
|
+
/ts-procedures:scaffold hono-api ProductApi
|
|
18
19
|
---
|
|
19
20
|
|
|
20
21
|
# Scaffold ts-procedures Code
|
|
@@ -42,6 +43,7 @@ Parse `$ARGUMENTS` as `<type> <Name>` (case-insensitive type, PascalCase Name).
|
|
|
42
43
|
| `express-rpc` | `templates/express-rpc.md` | `{{Name}}.rpc.ts`, `{{Name}}.rpc.test.ts` |
|
|
43
44
|
| `hono-rpc` | `templates/hono-rpc.md` | `{{Name}}.rpc.ts`, `{{Name}}.rpc.test.ts` |
|
|
44
45
|
| `hono-stream` | `templates/hono-stream.md` | `{{Name}}.stream-rpc.ts`, `{{Name}}.stream-rpc.test.ts` |
|
|
46
|
+
| `hono-api` | `templates/hono-api.md` | `{{Name}}.api.ts`, `{{Name}}.api.test.ts` |
|
|
45
47
|
|
|
46
48
|
## Rules
|
|
47
49
|
|