ts-procedures 5.16.0 → 6.0.1

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 (147) hide show
  1. package/README.md +2 -0
  2. package/agent_config/claude-code/agents/ts-procedures-architect.md +13 -6
  3. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +26 -4
  4. package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +87 -19
  5. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +162 -16
  6. package/agent_config/claude-code/skills/ts-procedures/patterns.md +179 -16
  7. package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
  8. package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +20 -12
  9. package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +2 -1
  10. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +22 -15
  11. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +20 -17
  12. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +20 -16
  13. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +20 -17
  14. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +16 -3
  15. package/agent_config/copilot/copilot-instructions.md +78 -12
  16. package/agent_config/cursor/cursorrules +78 -12
  17. package/build/client/call.d.ts +2 -1
  18. package/build/client/call.js +9 -1
  19. package/build/client/call.js.map +1 -1
  20. package/build/client/error-dispatch.d.ts +13 -0
  21. package/build/client/error-dispatch.js +26 -0
  22. package/build/client/error-dispatch.js.map +1 -0
  23. package/build/client/error-dispatch.test.d.ts +1 -0
  24. package/build/client/error-dispatch.test.js +56 -0
  25. package/build/client/error-dispatch.test.js.map +1 -0
  26. package/build/client/fetch-adapter.js +10 -4
  27. package/build/client/fetch-adapter.js.map +1 -1
  28. package/build/client/index.d.ts +2 -1
  29. package/build/client/index.js +5 -1
  30. package/build/client/index.js.map +1 -1
  31. package/build/client/stream.d.ts +2 -1
  32. package/build/client/stream.js +13 -3
  33. package/build/client/stream.js.map +1 -1
  34. package/build/client/typed-error-dispatch.test.d.ts +1 -0
  35. package/build/client/typed-error-dispatch.test.js +168 -0
  36. package/build/client/typed-error-dispatch.test.js.map +1 -0
  37. package/build/client/types.d.ts +37 -0
  38. package/build/codegen/e2e.test.js +9 -4
  39. package/build/codegen/e2e.test.js.map +1 -1
  40. package/build/codegen/emit-client-runtime.js +4 -0
  41. package/build/codegen/emit-client-runtime.js.map +1 -1
  42. package/build/codegen/emit-errors.d.ts +17 -6
  43. package/build/codegen/emit-errors.integration.test.d.ts +1 -0
  44. package/build/codegen/emit-errors.integration.test.js +162 -0
  45. package/build/codegen/emit-errors.integration.test.js.map +1 -0
  46. package/build/codegen/emit-errors.js +50 -39
  47. package/build/codegen/emit-errors.js.map +1 -1
  48. package/build/codegen/emit-errors.test.js +75 -78
  49. package/build/codegen/emit-errors.test.js.map +1 -1
  50. package/build/codegen/emit-index.d.ts +7 -0
  51. package/build/codegen/emit-index.js +26 -4
  52. package/build/codegen/emit-index.js.map +1 -1
  53. package/build/codegen/emit-index.test.js +55 -23
  54. package/build/codegen/emit-index.test.js.map +1 -1
  55. package/build/codegen/emit-scope.d.ts +8 -0
  56. package/build/codegen/emit-scope.js +82 -7
  57. package/build/codegen/emit-scope.js.map +1 -1
  58. package/build/codegen/pipeline.js +22 -2
  59. package/build/codegen/pipeline.js.map +1 -1
  60. package/build/implementations/http/doc-registry.d.ts +17 -1
  61. package/build/implementations/http/doc-registry.js +47 -79
  62. package/build/implementations/http/doc-registry.js.map +1 -1
  63. package/build/implementations/http/doc-registry.test.js +149 -16
  64. package/build/implementations/http/doc-registry.test.js.map +1 -1
  65. package/build/implementations/http/error-taxonomy.d.ts +249 -0
  66. package/build/implementations/http/error-taxonomy.js +252 -0
  67. package/build/implementations/http/error-taxonomy.js.map +1 -0
  68. package/build/implementations/http/error-taxonomy.test.d.ts +1 -0
  69. package/build/implementations/http/error-taxonomy.test.js +399 -0
  70. package/build/implementations/http/error-taxonomy.test.js.map +1 -0
  71. package/build/implementations/http/express-rpc/error-taxonomy.test.d.ts +1 -0
  72. package/build/implementations/http/express-rpc/error-taxonomy.test.js +83 -0
  73. package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +1 -0
  74. package/build/implementations/http/express-rpc/index.d.ts +39 -8
  75. package/build/implementations/http/express-rpc/index.js +39 -8
  76. package/build/implementations/http/express-rpc/index.js.map +1 -1
  77. package/build/implementations/http/hono-api/error-taxonomy.test.d.ts +1 -0
  78. package/build/implementations/http/hono-api/error-taxonomy.test.js +137 -0
  79. package/build/implementations/http/hono-api/error-taxonomy.test.js.map +1 -0
  80. package/build/implementations/http/hono-api/index.d.ts +38 -1
  81. package/build/implementations/http/hono-api/index.js +32 -0
  82. package/build/implementations/http/hono-api/index.js.map +1 -1
  83. package/build/implementations/http/hono-rpc/error-taxonomy.test.d.ts +1 -0
  84. package/build/implementations/http/hono-rpc/error-taxonomy.test.js +64 -0
  85. package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +1 -0
  86. package/build/implementations/http/hono-rpc/index.d.ts +34 -7
  87. package/build/implementations/http/hono-rpc/index.js +31 -4
  88. package/build/implementations/http/hono-rpc/index.js.map +1 -1
  89. package/build/implementations/http/hono-stream/error-taxonomy.test.d.ts +1 -0
  90. package/build/implementations/http/hono-stream/error-taxonomy.test.js +87 -0
  91. package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +1 -0
  92. package/build/implementations/http/hono-stream/index.d.ts +40 -3
  93. package/build/implementations/http/hono-stream/index.js +37 -10
  94. package/build/implementations/http/hono-stream/index.js.map +1 -1
  95. package/build/implementations/http/hono-stream/index.test.js +45 -18
  96. package/build/implementations/http/hono-stream/index.test.js.map +1 -1
  97. package/build/implementations/http/on-request-error.test.d.ts +1 -0
  98. package/build/implementations/http/on-request-error.test.js +173 -0
  99. package/build/implementations/http/on-request-error.test.js.map +1 -0
  100. package/build/implementations/http/route-errors.test.d.ts +1 -0
  101. package/build/implementations/http/route-errors.test.js +139 -0
  102. package/build/implementations/http/route-errors.test.js.map +1 -0
  103. package/build/implementations/types.d.ts +43 -3
  104. package/docs/client-and-codegen.md +105 -12
  105. package/docs/core.md +14 -5
  106. package/docs/http-integrations.md +138 -5
  107. package/docs/streaming.md +3 -1
  108. package/docs/superpowers/plans/2026-04-24-doc-registry-simplification.md +886 -0
  109. package/package.json +7 -2
  110. package/src/client/call.ts +10 -1
  111. package/src/client/error-dispatch.test.ts +72 -0
  112. package/src/client/error-dispatch.ts +27 -0
  113. package/src/client/fetch-adapter.ts +11 -5
  114. package/src/client/index.ts +9 -0
  115. package/src/client/stream.ts +14 -3
  116. package/src/client/typed-error-dispatch.test.ts +211 -0
  117. package/src/client/types.ts +42 -0
  118. package/src/codegen/e2e.test.ts +9 -4
  119. package/src/codegen/emit-client-runtime.ts +4 -0
  120. package/src/codegen/emit-errors.integration.test.ts +183 -0
  121. package/src/codegen/emit-errors.test.ts +91 -87
  122. package/src/codegen/emit-errors.ts +123 -41
  123. package/src/codegen/emit-index.test.ts +68 -24
  124. package/src/codegen/emit-index.ts +66 -4
  125. package/src/codegen/emit-scope.ts +124 -7
  126. package/src/codegen/pipeline.ts +25 -2
  127. package/src/implementations/http/README.md +21 -7
  128. package/src/implementations/http/doc-registry.test.ts +164 -16
  129. package/src/implementations/http/doc-registry.ts +58 -82
  130. package/src/implementations/http/error-taxonomy.test.ts +438 -0
  131. package/src/implementations/http/error-taxonomy.ts +361 -0
  132. package/src/implementations/http/express-rpc/README.md +23 -24
  133. package/src/implementations/http/express-rpc/error-taxonomy.test.ts +103 -0
  134. package/src/implementations/http/express-rpc/index.ts +75 -14
  135. package/src/implementations/http/hono-api/README.md +284 -0
  136. package/src/implementations/http/hono-api/error-taxonomy.test.ts +179 -0
  137. package/src/implementations/http/hono-api/index.ts +76 -1
  138. package/src/implementations/http/hono-rpc/README.md +20 -21
  139. package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +82 -0
  140. package/src/implementations/http/hono-rpc/index.ts +65 -9
  141. package/src/implementations/http/hono-stream/README.md +44 -25
  142. package/src/implementations/http/hono-stream/error-taxonomy.test.ts +98 -0
  143. package/src/implementations/http/hono-stream/index.test.ts +54 -18
  144. package/src/implementations/http/hono-stream/index.ts +83 -13
  145. package/src/implementations/http/on-request-error.test.ts +201 -0
  146. package/src/implementations/http/route-errors.test.ts +176 -0
  147. package/src/implementations/types.ts +43 -3
@@ -103,6 +103,173 @@ const { TransferFunds } = Create(
103
103
 
104
104
  ---
105
105
 
106
+ ## Error Taxonomy (declarative error-to-response mapping)
107
+
108
+ `defineErrorTaxonomy` replaces `instanceof` ladders inside `onError`. Register error classes once with their status code and serializer, pass to any HTTP builder. Works across `hono-api`, `hono-rpc`, `express-rpc`, and `hono-stream` pre-stream (mid-stream still uses `onMidStreamError`).
109
+
110
+ ```typescript
111
+ import { defineErrorTaxonomy } from 'ts-procedures/http-errors'
112
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
113
+
114
+ class UseCaseError extends Error {
115
+ constructor(readonly externalMsg: string, readonly internalMsg: string) {
116
+ super(externalMsg); this.name = 'UseCaseError'
117
+ Object.setPrototypeOf(this, UseCaseError.prototype)
118
+ }
119
+ }
120
+
121
+ const appErrors = defineErrorTaxonomy({
122
+ AuthError: { class: AuthError, statusCode: 401 }, // default toResponse → { name, message }
123
+ UseCaseError: {
124
+ class: UseCaseError,
125
+ statusCode: 422,
126
+ toResponse: (err) => ({ message: err.externalMsg }), // `name: 'UseCaseError'` auto-injected
127
+ onCatch: (err, { procedure }) => logger.error({ procedure: procedure.name, internal: err.internalMsg }),
128
+ },
129
+ // 3rd-party / non-subclassable errors — use `match:`
130
+ MongoDuplicateKey: {
131
+ match: (err): err is Error & { code: number } =>
132
+ err instanceof Error && (err as any).code === 11000,
133
+ statusCode: 409,
134
+ toResponse: () => ({ message: 'Resource already exists' }),
135
+ },
136
+ })
137
+
138
+ new HonoAPIAppBuilder({
139
+ errors: appErrors,
140
+ unknownError: {
141
+ toResponse: () => ({ name: 'InternalServerError', message: 'Unexpected error' }),
142
+ onCatch: (err, { procedure }) => logger.error({ procedure: procedure.name, err }),
143
+ },
144
+ }).register(API, (c) => ({ requestId: c.req.header('x-request-id') })).build()
145
+ ```
146
+
147
+ Handlers throw directly — no try/catch, no ladders:
148
+
149
+ ```typescript
150
+ API.Create('GetUser', { /* ... */ }, async (ctx, { pathParams }) => {
151
+ const user = await usersRepo.findById(pathParams.id)
152
+ if (!user) throw new UseCaseError('User not found', `repo returned null for ${pathParams.id}`)
153
+ return user
154
+ })
155
+ ```
156
+
157
+ Key rules:
158
+
159
+ - `defineErrorTaxonomy` topologically sorts `class:` entries so a subclass always resolves before its base — declaration order inside the helper no longer matters for inheritance chains. `match:` entries keep declared order.
160
+ - The core wraps any non-`ProcedureError` thrown from a handler into a `ProcedureError` with `.cause` set. The resolver unwraps this automatically so your taxonomy sees the real class.
161
+ - Two peer modes: declarative (`errors` + `unknownError`) or imperative (`onError`). Both are first-class. When neither is configured, the builder falls through to a hard default (`{ error: message }`, 500).
162
+ - `onCatch`'s `raw` is the framework request object (Hono `Context` / `{ req, res }` for Express) typed as `unknown` — cast at the use site.
163
+
164
+ ### Per-route error declaration (typed)
165
+
166
+ Narrow `APIConfig` / `RPCConfig` to your taxonomy's keys for compile-time typo protection, and declare which errors each procedure may emit. These populate the DocEnvelope per-route so generated clients can type `catch` blocks precisely.
167
+
168
+ ```typescript
169
+ import { APIConfig } from 'ts-procedures/http'
170
+ import { appErrors } from './errors/taxonomy'
171
+
172
+ type MyAPIConfig = APIConfig<keyof typeof appErrors & string>
173
+
174
+ const API = Procedures<Ctx, MyAPIConfig>()
175
+
176
+ API.Create('GetUser', {
177
+ path: '/users/:id',
178
+ method: 'get',
179
+ errors: ['UseCaseError', 'AuthError'], // typed against taxonomy keys; typos are TS errors
180
+ schema: { /* ... */ },
181
+ }, async (ctx, params) => { /* ... */ })
182
+ ```
183
+
184
+ Seed the DocEnvelope with both taxonomy errors and framework defaults in one call:
185
+
186
+ ```typescript
187
+ import { DocRegistry } from 'ts-procedures/http-docs'
188
+
189
+ const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' })
190
+ .from(apiApp)
191
+ .from(rpcApp)
192
+ .toJSON()
193
+ // envelope.errors: all registered + framework defaults (deduped)
194
+ // envelope.routes[i].errors: per-route subset declared on config.errors
195
+ ```
196
+
197
+ ```typescript
198
+ // For errors outside your taxonomy (middleware, infrastructure, doc-only):
199
+ const docsWithExtras = new DocRegistry({ errors: appErrors, basePath: '/api' })
200
+ .from(apiBuilder)
201
+ .documentError({ name: 'RateLimitExceeded', statusCode: 429, description: 'too many requests' })
202
+ ```
203
+
204
+ ### Typed catch blocks on the client (via codegen)
205
+
206
+ The codegen emits runtime error classes (not just types) and a registry object. `createApiClient` wires the registry automatically so non-2xx responses arrive as typed class instances.
207
+
208
+ Generated client (simplified):
209
+
210
+ ```typescript
211
+ // Generated _errors.ts
212
+ export namespace ApiErrors {
213
+ export class ApiProcedureError<TBody> extends Error { /* ... */ }
214
+ export class UseCaseError extends ApiProcedureError<UseCaseErrorBody> {
215
+ static fromResponse(body, meta): UseCaseError { /* ... */ }
216
+ }
217
+ export const ApiErrorRegistry = { UseCaseError, AuthError, /* ... */ }
218
+ }
219
+
220
+ // Generated index.ts
221
+ export function createApiClient(config) {
222
+ return createClient({ ...config, errorRegistry: ApiErrors.ApiErrorRegistry, scopes: createApiBindings })
223
+ }
224
+ ```
225
+
226
+ Consumer code:
227
+
228
+ ```typescript
229
+ import { createApiClient, ApiErrors, createFetchAdapter } from './generated'
230
+
231
+ const api = createApiClient({
232
+ adapter: createFetchAdapter({ /* ... */ }),
233
+ basePath: 'https://api.example.com',
234
+ })
235
+
236
+ try {
237
+ const user = await api.users.getUser({ pathParams: { id: 'u_123' } })
238
+ } catch (err) {
239
+ if (err instanceof ApiErrors.UseCaseError) {
240
+ // err.message is typed; err.body is typed to UseCaseErrorBody;
241
+ // err.status, err.procedureName, err.scope are available.
242
+ } else if (err instanceof ApiErrors.ApiProcedureError) {
243
+ // Catch-all for any generated service error.
244
+ } else {
245
+ // Transport error (network, non-JSON body) — still a ClientRequestError.
246
+ }
247
+ }
248
+ ```
249
+
250
+ Per-route error narrowing: when a route declares `errors: [...]`, the generated scope file emits an `Errors` type union the consumer can use to annotate their catch:
251
+
252
+ ```typescript
253
+ // Generated scope file includes:
254
+ export namespace Users {
255
+ export namespace GetUser {
256
+ export type Params = { /* ... */ }
257
+ export type Response = { /* ... */ }
258
+ export type Errors = ApiErrors.UseCaseError | ApiErrors.AuthError
259
+ }
260
+ }
261
+
262
+ // Consumer:
263
+ catch (err: unknown) {
264
+ // Cast is manual today (TS has no typed throws); the union documents
265
+ // which errors this specific route can throw.
266
+ const e = err as Users.GetUser.Errors | ClientRequestError
267
+ if (e instanceof ApiErrors.UseCaseError) { /* ... */ }
268
+ }
269
+ ```
270
+
271
+ ---
272
+
106
273
  ## Stream Procedures
107
274
 
108
275
  ```typescript
@@ -275,15 +442,13 @@ const { UpdateUser } = Create(
275
442
  async (ctx, params) => updateUser(params)
276
443
  )
277
444
 
278
- // 2. Build Express app
445
+ // 2. Build Express app — the taxonomy handles framework errors (ProcedureValidationError
446
+ // maps to 400 by default) and anything the app throws; unknownError is the last resort.
279
447
  const app = new ExpressRPCAppBuilder({
280
448
  pathPrefix: '/api',
281
- onError: (procedure, req, res, error) => {
282
- if (error instanceof ProcedureValidationError) {
283
- res.status(400).json({ error: error.message, details: error.errors })
284
- } else {
285
- res.status(500).json({ error: error.message })
286
- }
449
+ errors: appErrors, // from defineErrorTaxonomy({ ... })
450
+ unknownError: {
451
+ toResponse: (err) => ({ error: (err as Error).message }),
287
452
  },
288
453
  })
289
454
  .register(
@@ -437,11 +602,9 @@ API.Create('DeleteUser', {
437
602
 
438
603
  const app = new HonoAPIAppBuilder({
439
604
  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)
605
+ errors: appErrors, // taxonomy maps error classes to statuses/bodies; ProcedureValidationError → 400 by default
606
+ unknownError: {
607
+ toResponse: (err) => ({ error: (err as Error).message }),
445
608
  },
446
609
  })
447
610
  .register(API, (c) => ({
@@ -649,7 +812,7 @@ import { DocRegistry } from 'ts-procedures/http-docs'
649
812
  const docs = new DocRegistry({
650
813
  basePath: '/api',
651
814
  headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
652
- errors: DocRegistry.defaultErrors(),
815
+ errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged and deduped
653
816
  })
654
817
  .from(rpcBuilder)
655
818
  .from(apiBuilder)
@@ -668,7 +831,7 @@ app.get('/docs', (c) => c.json(docs.toJSON({
668
831
  **Key points:**
669
832
  - `from()` stores a reference — register builders before or after `.build()`
670
833
  - `toJSON()` reads docs lazily, so late-registered procedures are included
671
- - `DocRegistry.defaultErrors()` provides error schemas for all 4 procedure error types
834
+ - Pass your `ErrorTaxonomy` directly to `errors` the constructor auto-merges framework defaults and dedupes; opt out with `includeDefaults: false`
672
835
  - Accepts any object satisfying `{ readonly docs: AnyHttpRouteDoc[] }` — not limited to built-in builders
673
836
 
674
837
  ---
@@ -754,7 +917,7 @@ onRequestStart → factoryContext() → handler() → onSuccess → onRequestEnd
754
917
  ### Streaming (HonoStreamAppBuilder)
755
918
  ```
756
919
  onRequestStart → factoryContext() → params validation
757
- onPreStreamError (if invalid) → onRequestEnd
920
+ onError (if invalid) → onRequestEnd
758
921
  → onStreamStart → handler yields → onStreamEnd → onRequestEnd
759
922
  → onMidStreamError (if throw) → onStreamEnd → onRequestEnd
760
923
  ```
@@ -770,7 +933,7 @@ import { DocRegistry } from 'ts-procedures/http-docs'
770
933
  const docs = new DocRegistry({
771
934
  basePath: '/api',
772
935
  headers: [{ name: 'Authorization', description: 'Bearer token' }],
773
- errors: DocRegistry.defaultErrors(),
936
+ errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged
774
937
  })
775
938
  .from(rpcBuilder)
776
939
  .from(apiBuilder)
@@ -14,7 +14,7 @@ Parse `$ARGUMENTS` as a file or directory path. If a directory, review all `.ts`
14
14
  ## Instructions
15
15
 
16
16
  1. Read the target file(s).
17
- 2. Identify ts-procedures imports (`ts-procedures`, `ts-procedures/express-rpc`, `ts-procedures/hono-rpc`, `ts-procedures/hono-stream`, `ts-procedures/hono-api`, `ts-procedures/http`, `ts-procedures/http-docs`) to determine file types.
17
+ 2. Identify ts-procedures imports (`ts-procedures`, `ts-procedures/express-rpc`, `ts-procedures/hono-rpc`, `ts-procedures/hono-stream`, `ts-procedures/hono-api`, `ts-procedures/http`, `ts-procedures/http-docs`, `ts-procedures/http-errors`, `ts-procedures/client`, `ts-procedures/codegen`) to determine file types.
18
18
  3. Check each file against the categorized checklist in [checklist.md](checklist.md).
19
19
  4. For detailed code examples of each violation pattern, reference [anti-patterns.md](../ts-procedures/anti-patterns.md) — it shows 20 common mistakes with before/after code fixes and severity ratings.
20
20
  5. Output findings grouped by severity.
@@ -62,26 +62,29 @@
62
62
  ### CRITICAL
63
63
  - [ ] Standard `Create` procedures registered with `ExpressRPCAppBuilder` or `HonoRPCAppBuilder`, NOT `HonoStreamAppBuilder`
64
64
  - [ ] Stream `CreateStream` procedures registered with `HonoStreamAppBuilder`, NOT the RPC builders
65
- - [ ] `onError` callback handles `ProcedureValidationError` and `ProcedureError` differently
65
+ - [ ] Custom error classes are registered via `defineErrorTaxonomy` + builder's `errors` config — NOT hand-written `onError` instanceof ladders (anti-pattern #20)
66
+ - [ ] If a handler throws a non-`ProcedureError`, its class is either in the taxonomy or intentionally caught by `unknownError`
66
67
 
67
68
  ### WARNING
68
69
  - [ ] `pathPrefix` set consistently across builders
69
70
  - [ ] `scope` and `version` set on all procedures when using `RPCConfig`
70
71
  - [ ] Uses `extendProcedureDoc` for documentation generation instead of manual doc building
71
72
  - [ ] `onRequestEnd` used for cleanup/logging, not business logic
73
+ - [ ] `RPCConfig<keyof typeof appErrors & string>` used when the app wants compile-time typo protection on per-route `errors: [...]`
72
74
 
73
75
  ### SUGGESTION
74
76
  - [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
75
- - [ ] Uses `DocRegistry` to compose docs from multiple builders instead of manual envelope assembly
77
+ - [ ] Uses `new DocRegistry({ errors: appErrors })` to compose docs from multiple builders
76
78
  - [ ] Lifecycle hooks used for observability (logging, metrics)
79
+ - [ ] Per-route `errors: [...]` declared so generated clients can narrow `catch` types
77
80
 
78
81
  ---
79
82
 
80
83
  ## Streaming Checks (HonoStreamAppBuilder)
81
84
 
82
85
  ### CRITICAL
83
- - [ ] `onPreStreamError` handles validation errors before streaming starts (returns HTTP response)
84
- - [ ] `onMidStreamError` handles runtime errors during streaming (yields error event)
86
+ - [ ] Pre-stream errors handled via either peer mode — `errors` + `unknownError` taxonomy OR `onError` callback (the hono-stream `onPreStreamError` was renamed to `onError` in v6)
87
+ - [ ] `onMidStreamError` handles runtime errors during streaming (yields error event) — mid-stream is NOT covered by the taxonomy since HTTP status is already committed
85
88
  - [ ] Stream handler does not assume `signal.reason` is always `'stream-completed'` — check for external abort
86
89
 
87
90
  ### WARNING
@@ -98,20 +101,21 @@
98
101
  ## API Builder Checks (HonoAPIAppBuilder)
99
102
 
100
103
  ### CRITICAL
101
- - [ ] `build()` is `await`ed — returns `Promise<Hono>`, not `Hono`
104
+ - [ ] `build()` is called synchronously it returns `Hono`, NOT `Promise<Hono>`. Do not `await` it.
102
105
  - [ ] Path param names in route template match `schema.input.pathParams` property names exactly
103
106
  - [ ] Does not define both `schema.params` and `schema.input` on the same procedure
104
- - [ ] `onError` callback handles `ProcedureValidationError` and `ProcedureError` differently
107
+ - [ ] Custom error classes registered via `defineErrorTaxonomy` + builder `errors` config — not hand-written `onError` ladders
105
108
 
106
109
  ### WARNING
107
110
  - [ ] Uses `schema.input` channels appropriate for the HTTP method (no `body` on GET/HEAD)
108
111
  - [ ] `pathPrefix` set consistently
109
112
  - [ ] `successStatus` overridden only when default (POST→201, DELETE→204) is wrong
110
113
  - [ ] Uses `APIInput` type constraint (`satisfies APIInput`) to catch channel name typos
114
+ - [ ] `APIConfig<keyof typeof appErrors & string>` used when the app wants compile-time typo protection on per-route `errors: [...]`
111
115
 
112
116
  ### SUGGESTION
113
117
  - [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
114
- - [ ] Uses `DocRegistry` to compose docs across builders instead of manual assembly
118
+ - [ ] Uses `new DocRegistry({ errors: appErrors })` to compose docs across builders instead of manual assembly
115
119
  - [ ] Custom `queryParser` provided if complex query string formats needed
116
120
  - [ ] Lifecycle hooks used for observability (logging, metrics)
117
121
 
@@ -120,18 +124,22 @@
120
124
  ## Error Handling Checks
121
125
 
122
126
  ### CRITICAL
123
- - [ ] Handler errors use `ctx.error(message, meta?)` — not `throw new Error()`
127
+ - [ ] Handler errors use `ctx.error(message, meta?)` OR throw a class registered in the app's `defineErrorTaxonomy` never `throw new Error()` directly
124
128
  - [ ] Does not catch and swallow errors without re-throwing (hides failures from caller)
125
- - [ ] HTTP builder has `onError` callback — default behavior may expose internal details
129
+ - [ ] HTTP builder has either an `errors` taxonomy or an `onError` callback configured both are first-class peer modes; leaving both off uses the hard default `{ error: message }` 500, which may expose internal details
130
+ - [ ] `defineErrorTaxonomy` entries use `class:` for `instanceof` dispatch or `match:` for 3rd-party errors (e.g. MongoServerError) — never both on the same entry
126
131
 
127
132
  ### WARNING
128
- - [ ] `ProcedureValidationError` handled separately (400 status) from `ProcedureError` (4xx/5xx)
133
+ - [ ] Framework errors (`ProcedureValidationError` 400, `ProcedureError` → 500) are covered by the default taxonomy automatically; don't re-declare them unless overriding
134
+ - [ ] `toResponse` output either includes a `name` field or lets the resolver auto-inject `{ name: key }` — required for typed client dispatch
129
135
  - [ ] Error `meta` object contains useful context (error codes, IDs) — not sensitive data
136
+ - [ ] Stack traces and `internalMsg`-style fields are kept server-side via `onCatch`, never included in `toResponse` output
130
137
  - [ ] Stack traces not exposed in production responses
131
138
 
132
139
  ### SUGGESTION
133
- - [ ] Consistent error response shape across all procedures
134
- - [ ] Error codes documented for API consumers
140
+ - [ ] Consistent error response shape across all procedures (guaranteed by the taxonomy's `name` field)
141
+ - [ ] Error codes documented for API consumers (feed `description` and `schema` on each taxonomy entry)
142
+ - [ ] Per-route `errors: [...]` declared on each procedure config so the generated client gets narrowed `Errors` union types
135
143
 
136
144
  ---
137
145
 
@@ -38,7 +38,8 @@ If either argument is missing, ask the user for `<type>` and `<Name>`.
38
38
 
39
39
  ## Rules
40
40
 
41
- - Use `ctx.error()` for business logic errors, never throw raw `Error`.
41
+ - Use `ctx.error()` for ad-hoc business errors; for structured errors with status codes and typed client dispatch, throw custom error classes and register them via `defineErrorTaxonomy` in the builder's `errors` config (see the HTTP builder templates).
42
+ - Never throw raw `Error` — either use `ctx.error()` or a typed class registered in the taxonomy.
42
43
  - `schema.params` is validated at runtime; `schema.returnType` is documentation only.
43
44
  - Pass `ctx.signal` to all downstream async calls.
44
45
  - Stream handlers always have `ctx.signal` (guaranteed `AbortSignal`).
@@ -3,14 +3,13 @@
3
3
  ## Implementation — `{{Name}}.client.ts`
4
4
 
5
5
  ```typescript
6
- import { createClient, createFetchAdapter } from 'ts-procedures/client'
7
- import { createApiBindings, Api } from './generated/api'
8
- // With --service-name {{Name}}: import { create{{Name}}Bindings, {{Name}} } from './generated/api'
6
+ import { createFetchAdapter } from 'ts-procedures/client'
7
+ import { createApiClient, ApiErrors } from './generated/api'
8
+ // With --service-name {{Name}}: import { create{{Name}}Client, {{Name}}Errors } from './generated/api'
9
9
 
10
10
  // --- Optional: type per-request meta end-to-end via declaration merging ---
11
- // Self-contained clients augment './generated/_types' instead:
12
- // declare module './generated/_types' { interface RequestMeta { traceId: string } }
13
- declare module 'ts-procedures/client' {
11
+ // Self-contained clients (the default) augment './generated/_types':
12
+ declare module './generated/_types' {
14
13
  interface RequestMeta {
15
14
  // TODO: add your fields — typed in options.meta, ctx.request.meta, adapter req.meta
16
15
  // traceId?: string
@@ -18,12 +17,12 @@ declare module 'ts-procedures/client' {
18
17
  }
19
18
  }
20
19
 
21
- // Create the typed client
22
- export const {{name}}Client = createClient({
20
+ // Create the typed client — createApiClient wires the error registry
21
+ // automatically so non-2xx responses arrive as typed class instances you can
22
+ // catch with `instanceof ApiErrors.<ErrorName>`.
23
+ export const {{name}}Client = createApiClient({
23
24
  adapter: createFetchAdapter(),
24
25
  basePath: 'http://localhost:3000', // TODO: configure base URL
25
- scopes: createApiBindings,
26
- // With --service-name {{Name}}: scopes: create{{Name}}Bindings,
27
26
  defaults: {
28
27
  // TODO: set client-wide defaults (overridden by per-call options)
29
28
  // timeout: 30_000,
@@ -45,6 +44,15 @@ export const {{name}}Client = createClient({
45
44
  },
46
45
  })
47
46
 
47
+ // --- Typed error handling ---
48
+ // try {
49
+ // const user = await {{name}}Client.users.GetUser({ pathParams: { id: '123' } })
50
+ // } catch (err) {
51
+ // if (err instanceof ApiErrors.UseCaseError) { /* err.body typed; err.status, procedureName, scope */ }
52
+ // else if (err instanceof ApiErrors.ApiProcedureError) { /* catch-all for service errors */ }
53
+ // else { /* transport error — ClientRequestError */ }
54
+ // }
55
+
48
56
  // --- RPC call example ---
49
57
  // const user = await {{name}}Client.users.GetUser({ userId: '123' })
50
58
 
@@ -105,11 +113,10 @@ describe('{{Name}} Client', () => {
105
113
  // expect(result.id).toBe('test-id')
106
114
  })
107
115
 
108
- test('handles validation errors from the server', async () => {
109
- // TODO: call with invalid params and verify error handling
110
- // await expect(
111
- // {{name}}Client.users.GetUser({} as any)
112
- // ).rejects.toThrow()
116
+ test('dispatches typed errors from the server via the registry', async () => {
117
+ // TODO: call with invalid params and verify typed error dispatch
118
+ // const call = {{name}}Client.users.GetUser({} as any)
119
+ // await expect(call).rejects.toBeInstanceOf(ApiErrors.ProcedureValidationError)
113
120
  })
114
121
 
115
122
  test('streams data with typed yields', async () => {
@@ -3,7 +3,8 @@
3
3
  ## Implementation — `{{Name}}.rpc.ts`
4
4
 
5
5
  ```typescript
6
- import { Procedures, ProcedureError, ProcedureValidationError } from 'ts-procedures'
6
+ import { Procedures } from 'ts-procedures'
7
+ import { defineErrorTaxonomy } from 'ts-procedures/express-rpc'
7
8
  import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
8
9
  import type { RPCConfig } from 'ts-procedures/http'
9
10
  import { Type } from 'typebox'
@@ -60,26 +61,28 @@ export const { ListItems } = RPC.Create(
60
61
  }
61
62
  )
62
63
 
64
+ // ─── Error Taxonomy ───────────────────────────────────────
65
+ // Declare the error classes this service throws. Framework errors
66
+ // (ProcedureValidationError → 400, ctx.error() → 500) are caught by the
67
+ // default taxonomy automatically. Add your own classes here — handlers just
68
+ // `throw` them and the builder serializes via this map. See
69
+ // docs/http-integrations.md#error-handling for the full contract.
70
+
71
+ const {{name}}Errors = defineErrorTaxonomy({
72
+ // Example — replace with your app's error classes:
73
+ // NotFoundError: { class: NotFoundError, statusCode: 404 },
74
+ // AuthError: { class: AuthError, statusCode: 401 },
75
+ })
76
+
63
77
  // ─── Express App Builder ──────────────────────────────────
64
78
 
65
79
  export const {{name}}App = new ExpressRPCAppBuilder({
66
80
  pathPrefix: '/api',
67
- onError: (procedure, req, res, error) => {
68
- if (error instanceof ProcedureValidationError) {
69
- res.status(400).json({
70
- error: error.message,
71
- details: error.errors,
72
- procedure: error.procedureName,
73
- })
74
- } else if (error instanceof ProcedureError) {
75
- res.status(422).json({
76
- error: error.message,
77
- meta: error.meta,
78
- procedure: error.procedureName,
79
- })
80
- } else {
81
- res.status(500).json({ error: 'Internal server error' })
82
- }
81
+ errors: {{name}}Errors,
82
+ unknownError: {
83
+ statusCode: 500,
84
+ toResponse: () => ({ name: 'InternalServerError', message: 'Unexpected error' }),
85
+ onCatch: (err, { procedure }) => console.error(`[${procedure.name}]`, err),
83
86
  },
84
87
  })
85
88
  .register(RPC, async (req) => ({
@@ -3,7 +3,8 @@
3
3
  ## Implementation — `{{Name}}.api.ts`
4
4
 
5
5
  ```typescript
6
- import { Procedures, ProcedureError, ProcedureValidationError } from 'ts-procedures'
6
+ import { Procedures } from 'ts-procedures'
7
+ import { defineErrorTaxonomy } from 'ts-procedures/hono-api'
7
8
  import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
8
9
  import type { APIConfig, APIInput } from 'ts-procedures/http'
9
10
  import { Type } from 'typebox'
@@ -81,25 +82,28 @@ export const { DeleteItem } = API.Create(
81
82
  }
82
83
  )
83
84
 
85
+ // ─── Error Taxonomy ───────────────────────────────────────
86
+ // Declare the error classes this service throws. Framework errors
87
+ // (ProcedureValidationError → 400, ctx.error() → 500) are caught by the
88
+ // default taxonomy automatically. Add your own classes here — handlers just
89
+ // `throw` them and the builder serializes via this map. See
90
+ // docs/http-integrations.md#error-handling for the full contract.
91
+
92
+ const {{name}}Errors = defineErrorTaxonomy({
93
+ // Example — replace with your app's error classes:
94
+ // NotFoundError: { class: NotFoundError, statusCode: 404 },
95
+ // AuthError: { class: AuthError, statusCode: 401 },
96
+ })
97
+
84
98
  // ─── Hono App Builder ─────────────────────────────────────
85
99
 
86
100
  export const {{name}}App = new HonoAPIAppBuilder({
87
101
  pathPrefix: '/api',
88
- onError: (procedure, c, error) => {
89
- if (error instanceof ProcedureValidationError) {
90
- return c.json({
91
- error: error.message,
92
- details: error.errors,
93
- procedure: error.procedureName,
94
- }, 400)
95
- } else if (error instanceof ProcedureError) {
96
- return c.json({
97
- error: error.message,
98
- meta: error.meta,
99
- procedure: error.procedureName,
100
- }, 422)
101
- }
102
- return c.json({ error: 'Internal server error' }, 500)
102
+ errors: {{name}}Errors,
103
+ unknownError: {
104
+ statusCode: 500,
105
+ toResponse: () => ({ name: 'InternalServerError', message: 'Unexpected error' }),
106
+ onCatch: (err, { procedure }) => console.error(`[${procedure.name}]`, err),
103
107
  },
104
108
  })
105
109
  .register(API, (c) => ({
@@ -3,8 +3,8 @@
3
3
  ## Implementation — `{{Name}}.rpc.ts`
4
4
 
5
5
  ```typescript
6
- import { Procedures, ProcedureError, ProcedureValidationError } from 'ts-procedures'
7
- import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
6
+ import { Procedures } from 'ts-procedures'
7
+ import { HonoRPCAppBuilder, defineErrorTaxonomy } from 'ts-procedures/hono-rpc'
8
8
  import type { RPCConfig } from 'ts-procedures/http'
9
9
  import { Type } from 'typebox'
10
10
 
@@ -60,25 +60,28 @@ export const { ListItems } = RPC.Create(
60
60
  }
61
61
  )
62
62
 
63
+ // ─── Error Taxonomy ───────────────────────────────────────
64
+ // Declare the error classes this service throws. Framework errors
65
+ // (ProcedureValidationError → 400, ctx.error() → 500) are caught by the
66
+ // default taxonomy automatically. Add your own classes here — handlers just
67
+ // `throw` them and the builder serializes via this map. See
68
+ // docs/http-integrations.md#error-handling for the full contract.
69
+
70
+ const {{name}}Errors = defineErrorTaxonomy({
71
+ // Example — replace with your app's error classes:
72
+ // NotFoundError: { class: NotFoundError, statusCode: 404 },
73
+ // AuthError: { class: AuthError, statusCode: 401 },
74
+ })
75
+
63
76
  // ─── Hono App Builder ─────────────────────────────────────
64
77
 
65
78
  export const {{name}}App = new HonoRPCAppBuilder({
66
79
  pathPrefix: '/api',
67
- onError: (procedure, c, error) => {
68
- if (error instanceof ProcedureValidationError) {
69
- return c.json({
70
- error: error.message,
71
- details: error.errors,
72
- procedure: error.procedureName,
73
- }, 400)
74
- } else if (error instanceof ProcedureError) {
75
- return c.json({
76
- error: error.message,
77
- meta: error.meta,
78
- procedure: error.procedureName,
79
- }, 422)
80
- }
81
- return c.json({ error: 'Internal server error' }, 500)
80
+ errors: {{name}}Errors,
81
+ unknownError: {
82
+ statusCode: 500,
83
+ toResponse: () => ({ name: 'InternalServerError', message: 'Unexpected error' }),
84
+ onCatch: (err, { procedure }) => console.error(`[${procedure.name}]`, err),
82
85
  },
83
86
  })
84
87
  .register(RPC, (c) => ({
@@ -4,7 +4,7 @@
4
4
 
5
5
  ```typescript
6
6
  import { Procedures } from 'ts-procedures'
7
- import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
7
+ import { HonoStreamAppBuilder, sse, defineErrorTaxonomy } from 'ts-procedures/hono-stream'
8
8
  import type { RPCConfig } from 'ts-procedures/http'
9
9
  import { Type } from 'typebox'
10
10
 
@@ -60,13 +60,26 @@ async function pollForEvents(channel: string, opts?: { signal?: AbortSignal }) {
60
60
  return { type: 'update', data: { value: Math.random() } }
61
61
  }
62
62
 
63
+ // ─── Error Taxonomy (pre-stream) ──────────────────────────
64
+ // Pre-stream errors (validation, context resolution) flow through the
65
+ // taxonomy — framework errors are caught by the default taxonomy automatically.
66
+ // Mid-stream errors (thrown after the first yield) still go through
67
+ // onMidStreamError since the HTTP status is already committed.
68
+
69
+ const {{name}}Errors = defineErrorTaxonomy({
70
+ // Example — replace with your app's error classes:
71
+ // AuthError: { class: AuthError, statusCode: 401 },
72
+ })
73
+
63
74
  // ─── Hono Stream App Builder ──────────────────────────────
64
75
 
65
76
  export const {{name}}StreamApp = new HonoStreamAppBuilder({
66
77
  pathPrefix: '/api',
67
78
  defaultStreamMode: 'sse', // or 'text' for newline-delimited JSON
68
- onPreStreamError: (procedure, c, error) => {
69
- return c.json({ error: error.message }, 400)
79
+ errors: {{name}}Errors,
80
+ unknownError: {
81
+ statusCode: 500,
82
+ toResponse: () => ({ name: 'InternalServerError', message: 'Unexpected error' }),
70
83
  },
71
84
  onMidStreamError: (procedure, c, error) => ({
72
85
  data: { type: 'error', payload: { message: error.message }, timestamp: Date.now() },