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.
Files changed (38) hide show
  1. package/README.md +90 -0
  2. package/agent_config/claude-code/agents/ts-procedures-architect.md +15 -0
  3. package/agent_config/claude-code/skills/guide/anti-patterns.md +106 -0
  4. package/agent_config/claude-code/skills/guide/api-reference.md +150 -4
  5. package/agent_config/claude-code/skills/guide/patterns.md +155 -0
  6. package/agent_config/claude-code/skills/review/checklist.md +22 -0
  7. package/agent_config/claude-code/skills/scaffold/SKILL.md +3 -1
  8. package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +169 -0
  9. package/agent_config/copilot/copilot-instructions.md +35 -0
  10. package/agent_config/cursor/cursorrules +35 -0
  11. package/build/implementations/http/hono-api/index.d.ts +102 -0
  12. package/build/implementations/http/hono-api/index.js +339 -0
  13. package/build/implementations/http/hono-api/index.js.map +1 -0
  14. package/build/implementations/http/hono-api/index.test.d.ts +1 -0
  15. package/build/implementations/http/hono-api/index.test.js +983 -0
  16. package/build/implementations/http/hono-api/index.test.js.map +1 -0
  17. package/build/implementations/http/hono-api/types.d.ts +13 -0
  18. package/build/implementations/http/hono-api/types.js +2 -0
  19. package/build/implementations/http/hono-api/types.js.map +1 -0
  20. package/build/implementations/types.d.ts +44 -0
  21. package/build/index.d.ts +28 -6
  22. package/build/index.js +28 -0
  23. package/build/index.js.map +1 -1
  24. package/build/schema/compute-schema.d.ts +5 -0
  25. package/build/schema/compute-schema.js +8 -1
  26. package/build/schema/compute-schema.js.map +1 -1
  27. package/build/schema/parser.d.ts +6 -5
  28. package/build/schema/parser.js +54 -0
  29. package/build/schema/parser.js.map +1 -1
  30. package/package.json +8 -3
  31. package/src/implementations/http/README.md +45 -2
  32. package/src/implementations/http/hono-api/index.test.ts +1328 -0
  33. package/src/implementations/http/hono-api/index.ts +461 -0
  34. package/src/implementations/http/hono-api/types.ts +16 -0
  35. package/src/implementations/types.ts +52 -0
  36. package/src/index.ts +87 -10
  37. package/src/schema/compute-schema.ts +23 -2
  38. 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: { params?: string; returnType?: string; yieldType?: string }) => void
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