ts-procedures 5.15.0 → 6.0.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 +2 -0
- package/agent_config/claude-code/agents/ts-procedures-architect.md +13 -6
- package/agent_config/claude-code/skills/ts-procedures/SKILL.md +26 -4
- package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +85 -17
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +220 -9
- package/agent_config/claude-code/skills/ts-procedures/patterns.md +271 -16
- package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +20 -12
- package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +2 -1
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +53 -18
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +20 -17
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +20 -16
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +20 -17
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +16 -3
- package/agent_config/copilot/copilot-instructions.md +132 -19
- package/agent_config/cursor/cursorrules +132 -19
- package/build/client/call.d.ts +19 -9
- package/build/client/call.js +33 -19
- package/build/client/call.js.map +1 -1
- package/build/client/call.test.js +167 -17
- package/build/client/call.test.js.map +1 -1
- package/build/client/error-dispatch.d.ts +13 -0
- package/build/client/error-dispatch.js +26 -0
- package/build/client/error-dispatch.js.map +1 -0
- package/build/client/error-dispatch.test.d.ts +1 -0
- package/build/client/error-dispatch.test.js +56 -0
- package/build/client/error-dispatch.test.js.map +1 -0
- package/build/client/fetch-adapter.js +10 -4
- package/build/client/fetch-adapter.js.map +1 -1
- package/build/client/index.d.ts +2 -1
- package/build/client/index.js +22 -3
- package/build/client/index.js.map +1 -1
- package/build/client/index.test.js +104 -0
- package/build/client/index.test.js.map +1 -1
- package/build/client/resolve-options.d.ts +45 -0
- package/build/client/resolve-options.js +82 -0
- package/build/client/resolve-options.js.map +1 -0
- package/build/client/resolve-options.test.d.ts +1 -0
- package/build/client/resolve-options.test.js +158 -0
- package/build/client/resolve-options.test.js.map +1 -0
- package/build/client/stream.d.ts +19 -9
- package/build/client/stream.js +36 -21
- package/build/client/stream.js.map +1 -1
- package/build/client/stream.test.js +102 -46
- package/build/client/stream.test.js.map +1 -1
- package/build/client/typed-error-dispatch.test.d.ts +1 -0
- package/build/client/typed-error-dispatch.test.js +168 -0
- package/build/client/typed-error-dispatch.test.js.map +1 -0
- package/build/client/types.d.ts +105 -1
- package/build/client/types.js +1 -1
- package/build/codegen/e2e.test.js +150 -4
- package/build/codegen/e2e.test.js.map +1 -1
- package/build/codegen/emit-client-runtime.js +7 -0
- package/build/codegen/emit-client-runtime.js.map +1 -1
- package/build/codegen/emit-errors.d.ts +17 -6
- package/build/codegen/emit-errors.integration.test.d.ts +1 -0
- package/build/codegen/emit-errors.integration.test.js +162 -0
- package/build/codegen/emit-errors.integration.test.js.map +1 -0
- package/build/codegen/emit-errors.js +50 -39
- package/build/codegen/emit-errors.js.map +1 -1
- package/build/codegen/emit-errors.test.js +75 -78
- package/build/codegen/emit-errors.test.js.map +1 -1
- package/build/codegen/emit-index.d.ts +7 -0
- package/build/codegen/emit-index.js +26 -4
- package/build/codegen/emit-index.js.map +1 -1
- package/build/codegen/emit-index.test.js +55 -23
- package/build/codegen/emit-index.test.js.map +1 -1
- package/build/codegen/emit-scope.d.ts +8 -0
- package/build/codegen/emit-scope.js +82 -7
- package/build/codegen/emit-scope.js.map +1 -1
- package/build/codegen/pipeline.js +22 -2
- package/build/codegen/pipeline.js.map +1 -1
- package/build/implementations/http/doc-registry.d.ts +21 -0
- package/build/implementations/http/doc-registry.js +51 -78
- package/build/implementations/http/doc-registry.js.map +1 -1
- package/build/implementations/http/doc-registry.test.js +8 -6
- package/build/implementations/http/doc-registry.test.js.map +1 -1
- package/build/implementations/http/error-taxonomy.d.ts +240 -0
- package/build/implementations/http/error-taxonomy.js +230 -0
- package/build/implementations/http/error-taxonomy.js.map +1 -0
- package/build/implementations/http/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/error-taxonomy.test.js +399 -0
- package/build/implementations/http/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.js +83 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +39 -8
- package/build/implementations/http/express-rpc/index.js +39 -8
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/hono-api/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.js +137 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-api/index.d.ts +38 -1
- package/build/implementations/http/hono-api/index.js +32 -0
- package/build/implementations/http/hono-api/index.js.map +1 -1
- package/build/implementations/http/hono-rpc/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js +64 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.d.ts +34 -7
- package/build/implementations/http/hono-rpc/index.js +31 -4
- package/build/implementations/http/hono-rpc/index.js.map +1 -1
- package/build/implementations/http/hono-stream/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.js +87 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-stream/index.d.ts +40 -3
- package/build/implementations/http/hono-stream/index.js +37 -10
- package/build/implementations/http/hono-stream/index.js.map +1 -1
- package/build/implementations/http/hono-stream/index.test.js +45 -18
- package/build/implementations/http/hono-stream/index.test.js.map +1 -1
- package/build/implementations/http/on-request-error.test.d.ts +1 -0
- package/build/implementations/http/on-request-error.test.js +173 -0
- package/build/implementations/http/on-request-error.test.js.map +1 -0
- package/build/implementations/http/route-errors.test.d.ts +1 -0
- package/build/implementations/http/route-errors.test.js +140 -0
- package/build/implementations/http/route-errors.test.js.map +1 -0
- package/build/implementations/types.d.ts +30 -2
- package/docs/client-and-codegen.md +228 -14
- package/docs/core.md +14 -5
- package/docs/http-integrations.md +135 -4
- package/docs/streaming.md +3 -1
- package/package.json +7 -2
- package/src/client/call.test.ts +202 -29
- package/src/client/call.ts +50 -28
- package/src/client/error-dispatch.test.ts +72 -0
- package/src/client/error-dispatch.ts +27 -0
- package/src/client/fetch-adapter.ts +11 -5
- package/src/client/index.test.ts +117 -0
- package/src/client/index.ts +34 -8
- package/src/client/resolve-options.test.ts +205 -0
- package/src/client/resolve-options.ts +113 -0
- package/src/client/stream.test.ts +132 -107
- package/src/client/stream.ts +53 -27
- package/src/client/typed-error-dispatch.test.ts +211 -0
- package/src/client/types.ts +116 -2
- package/src/codegen/e2e.test.ts +160 -4
- package/src/codegen/emit-client-runtime.ts +7 -0
- package/src/codegen/emit-errors.integration.test.ts +183 -0
- package/src/codegen/emit-errors.test.ts +91 -87
- package/src/codegen/emit-errors.ts +123 -41
- package/src/codegen/emit-index.test.ts +68 -24
- package/src/codegen/emit-index.ts +66 -4
- package/src/codegen/emit-scope.ts +124 -7
- package/src/codegen/pipeline.ts +25 -2
- package/src/implementations/http/README.md +28 -5
- package/src/implementations/http/doc-registry.test.ts +10 -6
- package/src/implementations/http/doc-registry.ts +63 -80
- package/src/implementations/http/error-taxonomy.test.ts +438 -0
- package/src/implementations/http/error-taxonomy.ts +337 -0
- package/src/implementations/http/express-rpc/README.md +21 -22
- package/src/implementations/http/express-rpc/error-taxonomy.test.ts +103 -0
- package/src/implementations/http/express-rpc/index.ts +75 -14
- package/src/implementations/http/hono-api/README.md +284 -0
- package/src/implementations/http/hono-api/error-taxonomy.test.ts +179 -0
- package/src/implementations/http/hono-api/index.ts +76 -1
- package/src/implementations/http/hono-rpc/README.md +18 -19
- package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +82 -0
- package/src/implementations/http/hono-rpc/index.ts +65 -9
- package/src/implementations/http/hono-stream/README.md +44 -25
- package/src/implementations/http/hono-stream/error-taxonomy.test.ts +98 -0
- package/src/implementations/http/hono-stream/index.test.ts +54 -18
- package/src/implementations/http/hono-stream/index.ts +83 -13
- package/src/implementations/http/on-request-error.test.ts +201 -0
- package/src/implementations/http/route-errors.test.ts +177 -0
- package/src/implementations/types.ts +30 -2
package/README.md
CHANGED
|
@@ -50,6 +50,8 @@ const user2 = await procedure({}, { userId: '456' })
|
|
|
50
50
|
|
|
51
51
|
- **[Client Code Generation](docs/client-and-codegen.md)** — Generate type-safe client SDKs from your server's `DocRegistry`. CLI and programmatic API, adapters, hooks, streaming support, and self-contained mode.
|
|
52
52
|
|
|
53
|
+
- **[Typed Error Handling](docs/http-integrations.md#error-handling)** — Declarative `defineErrorTaxonomy` maps thrown error classes to HTTP responses across every builder; generated clients throw typed class instances you can catch with `instanceof`.
|
|
54
|
+
|
|
53
55
|
- **[AI Agent Setup](docs/ai-agent-setup.md)** — Built-in configuration for Claude Code, Cursor, and GitHub Copilot. Auto-updates on `npm install`.
|
|
54
56
|
|
|
55
57
|
Full documentation is available on [GitHub](https://github.com/thermsio/ts-procedures).
|
|
@@ -37,7 +37,10 @@ When asked to plan an API or procedure set:
|
|
|
37
37
|
- AJV is configured with `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`.
|
|
38
38
|
- `schema.params` and `schema.input` are mutually exclusive — defining both throws `ProcedureRegistrationError`.
|
|
39
39
|
- Path param names in route template (`:id`) must match `schema.input.pathParams` property names.
|
|
40
|
-
- Use `DocRegistry` to compose route docs from multiple builders — never manually wire `/docs` endpoints.
|
|
40
|
+
- Use `DocRegistry` to compose route docs from multiple builders — never manually wire `/docs` endpoints. Use `DocRegistry.fromTaxonomy(appErrors)` to seed envelope errors from your taxonomy + framework defaults in one call.
|
|
41
|
+
- Two first-class peer error-handling modes: **declarative taxonomy** (`defineErrorTaxonomy` + `errors` config) OR **imperative callback** (`onError`). Neither is deprecated. Pick the taxonomy for structured apps with typed client dispatch; pick `onError` for simple apps or full response control. Mixing both is allowed — the taxonomy handles what it covers, `onError` handles the tail. The anti-pattern is `instanceof` ladders inside `onError` (see anti-pattern #20 in the skill reference) — that's exactly what the taxonomy expresses declaratively.
|
|
42
|
+
- Per-route `errors: ['UseCaseError', ...]` narrows typed errors on the generated client. Declare via `APIConfig<keyof typeof appErrors & string>` / `RPCConfig<keyof typeof appErrors & string>` for compile-time typo protection.
|
|
43
|
+
- Generated `_errors.ts` emits real runtime classes extending `${Service}ProcedureError` — consumers catch with `instanceof ${Service}Errors.${Name}` and access `err.body`, `err.status`, `err.procedureName`, `err.scope`. Use the generated `create${Service}Client(config)` factory to wire the error registry automatically.
|
|
41
44
|
|
|
42
45
|
## Context Design Patterns
|
|
43
46
|
|
|
@@ -120,11 +123,15 @@ type AuthContext = { userId: string; requestId: string; db: Database }
|
|
|
120
123
|
- POST /api/users (HonoAPIAppBuilder, 201)
|
|
121
124
|
- DELETE /api/users/:id (HonoAPIAppBuilder, 204)
|
|
122
125
|
|
|
123
|
-
### Error Handling
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
126
|
+
### Error Handling (pick a mode, optionally combine)
|
|
127
|
+
- **Declarative mode (recommended for structured apps)**: `defineErrorTaxonomy({ AuthError: {class, 401}, NotFoundError: {class, 404}, ... })` wired via `errors` config. Drives typed client dispatch + DocEnvelope.
|
|
128
|
+
- **Imperative mode (simple apps or full response control)**: `onError: (procedure, c|req/res, err) => Response|void` — first-class peer.
|
|
129
|
+
- `unknownError` — fallback serializer for errors the taxonomy doesn't cover (pairs with declarative mode).
|
|
130
|
+
- `onRequestError` — cross-cutting observer for logging/tracing/metrics. Fires for every error, before dispatch.
|
|
131
|
+
- Input validation is automatic (default taxonomy: `ProcedureValidationError` → 400).
|
|
132
|
+
- Business errors: throw typed class instances — registered taxonomy classes auto-serialize, or handle in `onError`.
|
|
133
|
+
- Per-route: declare `errors: ['AuthError', ...]` on each route config so the generated client narrows `catch` types.
|
|
134
|
+
- Stream pre-errors: both peer modes apply. Mid-stream errors: `onMidStreamError` → yield error event, close stream.
|
|
128
135
|
```
|
|
129
136
|
|
|
130
137
|
## Next Steps
|
|
@@ -76,9 +76,29 @@ AJV config: `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`
|
|
|
76
76
|
| `ProcedureValidationError` | Schema params validation failure | `procedureName`, `errors[]` (AJV errors) |
|
|
77
77
|
| `ProcedureYieldValidationError` | Stream yield validation failure | `procedureName`, `errors[]` (AJV errors) |
|
|
78
78
|
| `ProcedureRegistrationError` | Invalid schema at registration time | `procedureName`, `message` |
|
|
79
|
+
| `${Service}ProcedureError` (generated, client) | Base class for all generated client error classes | `status`, `procedureName`, `scope`, `body` |
|
|
79
80
|
|
|
80
81
|
All errors include `definedAt` (file:line:column) and enhanced stack traces pointing to the procedure definition location.
|
|
81
82
|
|
|
83
|
+
## Error Taxonomy (HTTP builders)
|
|
84
|
+
|
|
85
|
+
Declare error classes once with `defineErrorTaxonomy` (from `ts-procedures/http-errors`) and pass to any HTTP builder via the `errors` option. Handlers `throw` their classes; the builder serializes to the configured status + body. This is the declarative peer of the imperative `onError` callback — both modes are first-class.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { defineErrorTaxonomy } from 'ts-procedures/http-errors'
|
|
89
|
+
|
|
90
|
+
const appErrors = defineErrorTaxonomy({
|
|
91
|
+
AuthError: { class: AuthError, statusCode: 401 },
|
|
92
|
+
UseCaseError: { class: UseCaseError, statusCode: 422, toResponse: (e) => ({ message: e.externalMsg }) },
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
new HonoAPIAppBuilder({ errors: appErrors, unknownError: { toResponse: () => ({ error: '...' }) } })
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Per-route narrowing: `APIConfig<keyof typeof appErrors & string>` / `RPCConfig<keyof typeof appErrors & string>` with a `errors: [...]` array on the config. Generated `_errors.ts` emits runtime classes — clients catch with `instanceof ${Service}Errors.${Name}` when using `create${Service}Client`.
|
|
99
|
+
|
|
100
|
+
Full contract: `docs/http-integrations.md § Error Handling` (canonical) and `api-reference.md § Error Taxonomy API`.
|
|
101
|
+
|
|
82
102
|
## HTTP Implementations
|
|
83
103
|
|
|
84
104
|
| Builder | Import | Transport |
|
|
@@ -112,10 +132,11 @@ const app = new ExpressRPCAppBuilder({ pathPrefix: '/api' })
|
|
|
112
132
|
| `onRequestStart` | Before context resolution |
|
|
113
133
|
| `onRequestEnd` | After response sent |
|
|
114
134
|
| `onSuccess` | After successful handler execution |
|
|
115
|
-
| `onError` |
|
|
135
|
+
| `onError` | Imperative error callback — first-class peer of the declarative `errors` taxonomy |
|
|
116
136
|
| `onStreamStart` | Before first yield (HonoStreamAppBuilder) |
|
|
117
137
|
| `onStreamEnd` | After stream closes (HonoStreamAppBuilder) |
|
|
118
|
-
| `
|
|
138
|
+
| `onError` (HonoStream) | Imperative pre-stream error callback — peer of `errors` taxonomy |
|
|
139
|
+
| `onRequestError` | Cross-cutting observer — fires for every caught error before dispatch |
|
|
119
140
|
| `onMidStreamError` | Error during streaming (return data to yield as final event) |
|
|
120
141
|
|
|
121
142
|
## Decision Framework
|
|
@@ -150,8 +171,9 @@ The npm package ships user-facing documentation with narrative explanations and
|
|
|
150
171
|
|------|--------|
|
|
151
172
|
| `docs/core.md` | Procedures factory, Create, CreateStream, schema.input, error handling |
|
|
152
173
|
| `docs/streaming.md` | Streaming procedures, AbortSignal, SSE patterns |
|
|
153
|
-
| `docs/http-integrations.md` | Express RPC, Hono RPC/Stream/API builders, DocRegistry |
|
|
154
|
-
| `docs/client-and-codegen.md` | Client code generation, createClient, CLI options |
|
|
174
|
+
| `docs/http-integrations.md` | Express RPC, Hono RPC/Stream/API builders, **error taxonomy (canonical)**, DocRegistry + `fromTaxonomy` |
|
|
175
|
+
| `docs/client-and-codegen.md` | Client code generation, `createApiClient`/`createClient`, typed error dispatch, per-route `Errors` unions, per-call options, client-level defaults, typed RequestMeta augmentation, CLI options |
|
|
176
|
+
| `CHANGELOG.md` | Release notes — see `[6.0.0]` for the current peer error-handling model (taxonomy + `onError` + `onRequestError`), per-route errors, and client runtime error classes. |
|
|
155
177
|
|
|
156
178
|
## Workflow
|
|
157
179
|
|
|
@@ -404,19 +404,21 @@ new HonoStreamAppBuilder({
|
|
|
404
404
|
})
|
|
405
405
|
```
|
|
406
406
|
|
|
407
|
-
**Fix:** Use `onPreStreamError`
|
|
407
|
+
**Fix:** Use either peer mode for pre-stream errors — the declarative taxonomy (`errors` + `unknownError`) or the imperative `onError` callback (renamed from `onPreStreamError` in v6). Mid-stream errors thrown after the first yield always go through `onMidStreamError` since HTTP status is already committed.
|
|
408
408
|
|
|
409
409
|
```typescript
|
|
410
410
|
// GOOD
|
|
411
|
+
import { defineErrorTaxonomy } from 'ts-procedures/http-errors'
|
|
412
|
+
|
|
411
413
|
new HonoStreamAppBuilder({
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
},
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
},
|
|
414
|
+
// Taxonomy handles all pre-stream errors (validation, auth, context)
|
|
415
|
+
errors: defineErrorTaxonomy({
|
|
416
|
+
AuthError: { class: AuthError, statusCode: 401 },
|
|
417
|
+
}),
|
|
418
|
+
unknownError: { toResponse: (err) => ({ error: (err as Error).message }) },
|
|
419
|
+
// Mid-stream still goes through onMidStreamError — the HTTP status is
|
|
420
|
+
// already committed, so errors become yields, not responses.
|
|
421
|
+
onMidStreamError: (proc, c, error) => ({ data: { error: error.message }, closeStream: true }),
|
|
420
422
|
})
|
|
421
423
|
```
|
|
422
424
|
|
|
@@ -496,21 +498,37 @@ builder.register(factory, async (req) => {
|
|
|
496
498
|
})
|
|
497
499
|
```
|
|
498
500
|
|
|
499
|
-
**Fix:**
|
|
501
|
+
**Fix:** Throw a typed error class from the context factory and register it in the builder's `errors` taxonomy — the builder auto-serializes.
|
|
500
502
|
|
|
501
503
|
```typescript
|
|
502
504
|
// GOOD
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
505
|
+
import { defineErrorTaxonomy } from 'ts-procedures/http-errors'
|
|
506
|
+
|
|
507
|
+
class AuthError extends Error {
|
|
508
|
+
constructor(message: string) { super(message); this.name = 'AuthError' }
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const appErrors = defineErrorTaxonomy({
|
|
512
|
+
AuthError: {
|
|
513
|
+
class: AuthError,
|
|
514
|
+
statusCode: 401,
|
|
515
|
+
toResponse: () => ({ message: 'Unauthorized' }),
|
|
510
516
|
},
|
|
511
517
|
})
|
|
518
|
+
|
|
519
|
+
const app = new ExpressRPCAppBuilder({
|
|
520
|
+
errors: appErrors,
|
|
521
|
+
unknownError: { toResponse: () => ({ error: 'Internal server error' }) },
|
|
522
|
+
})
|
|
523
|
+
.register(factory, async (req) => {
|
|
524
|
+
const user = await authenticate(req.headers.authorization)
|
|
525
|
+
if (!user) throw new AuthError('invalid token')
|
|
526
|
+
return { userId: user.id }
|
|
527
|
+
})
|
|
512
528
|
```
|
|
513
529
|
|
|
530
|
+
The imperative `onError: (procedure, req, res, error) => void` callback is a first-class peer — use it for apps that prefer a single response handler or when you want to handle the tail of errors the taxonomy doesn't cover.
|
|
531
|
+
|
|
514
532
|
---
|
|
515
533
|
|
|
516
534
|
## 18. Using Both schema.params and schema.input
|
|
@@ -592,6 +610,55 @@ Create('GetUser', {
|
|
|
592
610
|
|
|
593
611
|
---
|
|
594
612
|
|
|
613
|
+
## 20. Hand-writing `onError` instanceof ladders
|
|
614
|
+
|
|
615
|
+
**Problem:** Writing manual `instanceof` ladders inside `onError` to map each error class to a status + body. Every builder gets its own copy; generated clients see opaque `ClientRequestError` objects instead of typed instances; response shapes drift between routes.
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
// BAD — duplicated across builders, invisible to generated clients
|
|
619
|
+
new HonoAPIAppBuilder({
|
|
620
|
+
onError: (procedure, c, error) => {
|
|
621
|
+
if (error instanceof ValidationError) return c.json({ error: error.message }, 400)
|
|
622
|
+
if (error instanceof AuthError) return c.json({ error: 'Unauthorized' }, 401)
|
|
623
|
+
if (error instanceof UseCaseError) return c.json({ message: error.externalMsg }, 422)
|
|
624
|
+
return c.json({ error: 'Internal server error' }, 500)
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
**Fix:** Declare the taxonomy once with `defineErrorTaxonomy`, pass it via the builder's `errors` config, and handlers `throw` their own classes. The taxonomy also feeds the DocEnvelope so generated clients throw typed class instances.
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
// GOOD
|
|
633
|
+
import { defineErrorTaxonomy } from 'ts-procedures/http-errors'
|
|
634
|
+
|
|
635
|
+
const appErrors = defineErrorTaxonomy({
|
|
636
|
+
ValidationError: { class: ValidationError, statusCode: 400 },
|
|
637
|
+
AuthError: { class: AuthError, statusCode: 401 },
|
|
638
|
+
UseCaseError: {
|
|
639
|
+
class: UseCaseError,
|
|
640
|
+
statusCode: 422,
|
|
641
|
+
toResponse: (err) => ({ message: err.externalMsg }), // `name` auto-injected
|
|
642
|
+
},
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
new HonoAPIAppBuilder({
|
|
646
|
+
errors: appErrors,
|
|
647
|
+
unknownError: {
|
|
648
|
+
toResponse: () => ({ error: 'Internal server error' }),
|
|
649
|
+
onCatch: (err, { procedure }) => logger.error({ procedure: procedure.name, err }),
|
|
650
|
+
},
|
|
651
|
+
})
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
The same `errors` / `unknownError` shape plugs into every builder (`HonoAPIAppBuilder`, `HonoRPCAppBuilder`, `ExpressRPCAppBuilder`, `HonoStreamAppBuilder` pre-stream only). `ctx.error()` still works and is caught by the default taxonomy at 500.
|
|
655
|
+
|
|
656
|
+
**The anti-pattern is the ladder, not `onError`.** `onError` is a first-class peer of the taxonomy — it's perfectly fine for simple apps or when you want full response control. The problem is writing `instanceof` ladders *inside* it to route by error class, because that's exactly what the taxonomy is designed to express declaratively.
|
|
657
|
+
|
|
658
|
+
**Why:** The taxonomy is declarative (easy to audit), shared across builders, and drives client codegen — the generated `_errors.ts` emits real runtime classes so consumers can `catch (err) { if (err instanceof ApiErrors.UseCaseError) ... }` with full typing.
|
|
659
|
+
|
|
660
|
+
---
|
|
661
|
+
|
|
595
662
|
## Summary Table
|
|
596
663
|
|
|
597
664
|
| # | Anti-Pattern | Risk | Severity |
|
|
@@ -615,3 +682,4 @@ Create('GetUser', {
|
|
|
615
682
|
| 17 | Unhandled async context factory | Request crashes | WARNING |
|
|
616
683
|
| 18 | Both schema.params and schema.input | ProcedureRegistrationError at startup | CRITICAL |
|
|
617
684
|
| 19 | Mismatched path param names | Build-time error or confusing validation failures | CRITICAL |
|
|
685
|
+
| 20 | Hand-writing onError instanceof ladders | Drifting response shapes, untyped client errors | WARNING |
|
|
@@ -256,6 +256,125 @@ class ProcedureRegistrationError extends ProcedureError {
|
|
|
256
256
|
|
|
257
257
|
---
|
|
258
258
|
|
|
259
|
+
## Error Taxonomy API
|
|
260
|
+
|
|
261
|
+
Exported from `ts-procedures/http-errors` (and re-exported from every HTTP builder subpath: `ts-procedures/hono-api`, `ts-procedures/hono-rpc`, `ts-procedures/express-rpc`, `ts-procedures/hono-stream`).
|
|
262
|
+
|
|
263
|
+
### defineErrorTaxonomy(entries)
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
function defineErrorTaxonomy<T extends ErrorTaxonomy>(entries: T): T
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Identity helper that:
|
|
270
|
+
1. Validates each entry has exactly one discriminator (`class` xor `match`) — throws otherwise.
|
|
271
|
+
2. Topologically sorts `class:` entries so subclasses always precede base classes regardless of declared order. Predicate (`match:`) entries keep declared order.
|
|
272
|
+
|
|
273
|
+
### ErrorTaxonomyEntry
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
type ErrorTaxonomyEntry<TError = any, TBody = unknown> = {
|
|
277
|
+
class?: new (...args: any[]) => TError // instanceof discriminator — XOR with `match`
|
|
278
|
+
match?: (err: unknown) => err is TError // predicate discriminator — for 3rd-party errors
|
|
279
|
+
statusCode: number
|
|
280
|
+
description?: string // consumed by DocRegistry
|
|
281
|
+
schema?: Record<string, unknown> // response-body JSON Schema; consumed by DocRegistry
|
|
282
|
+
toResponse?: (err: TError, meta: { key: string }) => TBody
|
|
283
|
+
onCatch?: (
|
|
284
|
+
err: TError,
|
|
285
|
+
ctx: { procedure: TAnyProcedureRegistration; key: string; raw: unknown }
|
|
286
|
+
) => void | Promise<void>
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
type ErrorTaxonomy = Record<string, ErrorTaxonomyEntry>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
When `toResponse` is omitted, the body defaults to `{ name: key, message: err.message }`. When `toResponse` returns an object without a `name` field, the resolver auto-injects `{ name: key }` — wire-protocol consistency is guaranteed.
|
|
293
|
+
|
|
294
|
+
### defaultErrorTaxonomy
|
|
295
|
+
|
|
296
|
+
Pre-built taxonomy covering `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), and direct `ProcedureError` (500, unwrapped throws only — wrapped ones fall through so user taxonomy / `unknownError` sees the real error). Applied as a fallback after the user taxonomy unless `includeDefaults: false`.
|
|
297
|
+
|
|
298
|
+
### taxonomyToErrorDocs(taxonomy)
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
function taxonomyToErrorDocs(taxonomy: ErrorTaxonomy): ErrorDoc[]
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Converts a taxonomy to the `ErrorDoc[]` format consumed by `DocRegistry.errors` — single source of truth so the documented shape cannot drift from runtime behavior.
|
|
305
|
+
|
|
306
|
+
### resolveErrorResponse(params)
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
function resolveErrorResponse(params: {
|
|
310
|
+
err: unknown
|
|
311
|
+
userTaxonomy?: ErrorTaxonomy
|
|
312
|
+
includeDefaults?: boolean // default true
|
|
313
|
+
unknownError?: UnknownErrorConfig
|
|
314
|
+
procedure: TAnyProcedureRegistration
|
|
315
|
+
raw: unknown
|
|
316
|
+
}): { statusCode: number; body: unknown; runOnCatch: () => Promise<void> } | null
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Match order: user taxonomy → default taxonomy (when `includeDefaults`) → `unknownError`. Returns `null` when nothing matches — callers fall through to their builder's imperative `onError` callback and then the hard default. Unwraps `ProcedureError.cause` so user taxonomy matches against the real thrown error, not the wrapper. Side effects (`onCatch`) are deferred into `runOnCatch` so the caller decides when to execute them.
|
|
320
|
+
|
|
321
|
+
### UnknownErrorConfig
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
type UnknownErrorConfig = {
|
|
325
|
+
statusCode?: number // default 500
|
|
326
|
+
toResponse: (err: unknown) => unknown
|
|
327
|
+
onCatch?: (
|
|
328
|
+
err: unknown,
|
|
329
|
+
ctx: { procedure: TAnyProcedureRegistration; raw: unknown }
|
|
330
|
+
) => void | Promise<void>
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Client Error Registry API
|
|
337
|
+
|
|
338
|
+
Exported from `ts-procedures/client`. Generated clients hook their `${Service}ErrorRegistry` into `createClient` so non-2xx responses arrive as typed class instances.
|
|
339
|
+
|
|
340
|
+
### ErrorRegistry / ErrorFactory / ErrorResponseMeta
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
interface ErrorResponseMeta { status: number; procedureName: string; scope: string }
|
|
344
|
+
interface ErrorFactory { fromResponse(body: unknown, meta: ErrorResponseMeta): Error }
|
|
345
|
+
type ErrorRegistry = Record<string, ErrorFactory>
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### dispatchTypedError(registry, body, meta)
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
function dispatchTypedError(
|
|
352
|
+
registry: ErrorRegistry | undefined,
|
|
353
|
+
body: unknown,
|
|
354
|
+
meta: ErrorResponseMeta
|
|
355
|
+
): Error | null
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Returns a typed error instance when:
|
|
359
|
+
- `registry` is defined, AND
|
|
360
|
+
- `body` is an object with a string `name`, AND
|
|
361
|
+
- `registry[body.name].fromResponse(body, meta)` returns an `Error` subclass.
|
|
362
|
+
|
|
363
|
+
Otherwise returns `null`; callers fall back to `ClientRequestError`.
|
|
364
|
+
|
|
365
|
+
### CreateClientConfig.errorRegistry
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
interface CreateClientConfig<TScopes> {
|
|
369
|
+
// ...existing fields
|
|
370
|
+
errorRegistry?: ErrorRegistry
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Threaded into both `executeCall` and `executeStream`. The generated `create${Service}Client(config)` factory wires this automatically.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
259
378
|
## Schema Utilities
|
|
260
379
|
|
|
261
380
|
### extractJsonSchema(libSchema)
|
|
@@ -308,7 +427,11 @@ class ExpressRPCAppBuilder {
|
|
|
308
427
|
onRequestStart?: (req: express.Request) => void
|
|
309
428
|
onRequestEnd?: (req: express.Request, res: express.Response) => void
|
|
310
429
|
onSuccess?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response) => void
|
|
430
|
+
// Error handling — two peer modes plus a cross-cutting observer.
|
|
431
|
+
errors?: ErrorTaxonomy
|
|
432
|
+
unknownError?: UnknownErrorConfig
|
|
311
433
|
onError?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response, error: Error) => void
|
|
434
|
+
onRequestError?: (ctx: OnRequestErrorContext) => void | Promise<void>
|
|
312
435
|
})
|
|
313
436
|
|
|
314
437
|
register<TFactory>(
|
|
@@ -326,6 +449,8 @@ class ExpressRPCAppBuilder {
|
|
|
326
449
|
}
|
|
327
450
|
```
|
|
328
451
|
|
|
452
|
+
`OnRequestErrorContext` for Express: `{ err: unknown; procedure: TProcedureRegistration; raw: { req, res } }`.
|
|
453
|
+
|
|
329
454
|
### Route Path
|
|
330
455
|
|
|
331
456
|
`POST {pathPrefix}/{scope}/{kebab-name}/{version}`
|
|
@@ -357,7 +482,11 @@ class HonoRPCAppBuilder {
|
|
|
357
482
|
onRequestStart?: (c: Context) => void
|
|
358
483
|
onRequestEnd?: (c: Context) => void
|
|
359
484
|
onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
|
|
485
|
+
// Error handling — two peer modes plus a cross-cutting observer.
|
|
486
|
+
errors?: ErrorTaxonomy
|
|
487
|
+
unknownError?: UnknownErrorConfig
|
|
360
488
|
onError?: (procedure: TProcedureRegistration, c: Context, error: Error) => Response | Promise<Response>
|
|
489
|
+
onRequestError?: (ctx: OnRequestErrorContext) => void | Promise<void>
|
|
361
490
|
})
|
|
362
491
|
|
|
363
492
|
register<TFactory>(
|
|
@@ -372,6 +501,8 @@ class HonoRPCAppBuilder {
|
|
|
372
501
|
}
|
|
373
502
|
```
|
|
374
503
|
|
|
504
|
+
`OnRequestErrorContext` for Hono RPC: `{ err: unknown; procedure: TProcedureRegistration; raw: Context }`.
|
|
505
|
+
|
|
375
506
|
### Signal Injection
|
|
376
507
|
|
|
377
508
|
Uses `c.req.raw.signal` — the native Request signal from the Hono context.
|
|
@@ -392,7 +523,10 @@ class HonoStreamAppBuilder<TErrorData = unknown> {
|
|
|
392
523
|
onRequestEnd?: (c: Context) => void
|
|
393
524
|
onStreamStart?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
|
|
394
525
|
onStreamEnd?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
|
|
395
|
-
|
|
526
|
+
errors?: ErrorTaxonomy
|
|
527
|
+
unknownError?: UnknownErrorConfig
|
|
528
|
+
onError?: (procedure: TStreamProcedureRegistration, c: Context, error: Error) => Response | Promise<Response> // renamed from onPreStreamError in v6
|
|
529
|
+
onRequestError?: (ctx: { err: unknown; procedure: TStreamProcedureRegistration; raw: Context }) => void | Promise<void>
|
|
396
530
|
onMidStreamError?: (procedure: TStreamProcedureRegistration, c: Context, error: Error) => MidStreamErrorResult<TErrorData> | undefined
|
|
397
531
|
})
|
|
398
532
|
|
|
@@ -451,8 +585,8 @@ type MidStreamErrorResult<TErrorData = unknown> = {
|
|
|
451
585
|
|
|
452
586
|
### Error Handling
|
|
453
587
|
|
|
454
|
-
- **Pre-stream errors** (validation, context): Returns JSON error response (no streaming started).
|
|
455
|
-
- **Mid-stream errors** (generator throws): Yields error as final event, closes stream.
|
|
588
|
+
- **Pre-stream errors** (validation, context): Returns a JSON error response (no streaming started). Handle via either peer mode — `errors` taxonomy or `onError` callback. Observe via `onRequestError`.
|
|
589
|
+
- **Mid-stream errors** (generator throws): Yields error as final event, closes stream. Handle via `onMidStreamError` (the only mid-stream path — the HTTP status is already committed).
|
|
456
590
|
|
|
457
591
|
---
|
|
458
592
|
|
|
@@ -469,7 +603,11 @@ class HonoAPIAppBuilder {
|
|
|
469
603
|
onRequestStart?: (c: Context) => void
|
|
470
604
|
onRequestEnd?: (c: Context) => void
|
|
471
605
|
onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
|
|
606
|
+
// Error handling — two peer modes plus a cross-cutting observer.
|
|
607
|
+
errors?: ErrorTaxonomy
|
|
608
|
+
unknownError?: UnknownErrorConfig
|
|
472
609
|
onError?: (procedure: TProcedureRegistration, c: Context, error: Error) => Response | Promise<Response>
|
|
610
|
+
onRequestError?: (ctx: OnRequestErrorContext) => void | Promise<void>
|
|
473
611
|
})
|
|
474
612
|
|
|
475
613
|
register<TFactory>(
|
|
@@ -484,6 +622,8 @@ class HonoAPIAppBuilder {
|
|
|
484
622
|
}
|
|
485
623
|
```
|
|
486
624
|
|
|
625
|
+
`OnRequestErrorContext` for Hono API: `{ err: unknown; procedure: TProcedureRegistration; raw: Context }`.
|
|
626
|
+
|
|
487
627
|
### Route Path
|
|
488
628
|
|
|
489
629
|
Developer-defined via `APIConfig.path` — no auto-generation. Supports Hono path params (`:id`).
|
|
@@ -553,9 +693,12 @@ Uses `c.req.raw.signal` — native Request signal from Hono context.
|
|
|
553
693
|
### RPCConfig
|
|
554
694
|
|
|
555
695
|
```typescript
|
|
556
|
-
|
|
696
|
+
// Generic over valid taxonomy keys. Default TErrorKey = string (permissive).
|
|
697
|
+
// Narrow via `RPCConfig<keyof typeof appErrors & string>` for compile-time typo protection.
|
|
698
|
+
interface RPCConfig<TErrorKey extends string = string> {
|
|
557
699
|
scope: string | string[]
|
|
558
700
|
version: number
|
|
701
|
+
errors?: TErrorKey[] // Taxonomy keys this procedure may emit (informational; populates DocEnvelope).
|
|
559
702
|
}
|
|
560
703
|
```
|
|
561
704
|
|
|
@@ -570,6 +713,7 @@ interface RPCHttpRouteDoc extends RPCConfig {
|
|
|
570
713
|
body?: Record<string, unknown>
|
|
571
714
|
response?: Record<string, unknown>
|
|
572
715
|
}
|
|
716
|
+
errors?: string[] // Per-route subset, populated from config.errors.
|
|
573
717
|
}
|
|
574
718
|
```
|
|
575
719
|
|
|
@@ -586,16 +730,20 @@ interface StreamHttpRouteDoc extends RPCConfig {
|
|
|
586
730
|
yieldType?: Record<string, unknown>
|
|
587
731
|
returnType?: Record<string, unknown>
|
|
588
732
|
}
|
|
733
|
+
errors?: string[]
|
|
589
734
|
}
|
|
590
735
|
```
|
|
591
736
|
|
|
592
737
|
### APIConfig
|
|
593
738
|
|
|
594
739
|
```typescript
|
|
595
|
-
|
|
740
|
+
// Generic over valid taxonomy keys — same pattern as RPCConfig.
|
|
741
|
+
interface APIConfig<TErrorKey extends string = string> {
|
|
596
742
|
path: string
|
|
597
743
|
method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'
|
|
598
744
|
successStatus?: number
|
|
745
|
+
scope?: string
|
|
746
|
+
errors?: TErrorKey[]
|
|
599
747
|
}
|
|
600
748
|
```
|
|
601
749
|
|
|
@@ -612,6 +760,7 @@ interface APIHttpRouteDoc extends APIConfig {
|
|
|
612
760
|
headers?: Record<string, unknown>
|
|
613
761
|
response?: Record<string, unknown>
|
|
614
762
|
}
|
|
763
|
+
errors?: string[]
|
|
615
764
|
}
|
|
616
765
|
```
|
|
617
766
|
|
|
@@ -660,7 +809,16 @@ class DocRegistry {
|
|
|
660
809
|
transform?: (envelope: DocEnvelope) => T
|
|
661
810
|
}): T
|
|
662
811
|
|
|
812
|
+
// Framework error defaults derived from defaultErrorTaxonomy — single source
|
|
813
|
+
// of truth so runtime and documented shapes cannot drift.
|
|
663
814
|
static defaultErrors(): ErrorDoc[]
|
|
815
|
+
|
|
816
|
+
// Convenience: seed envelope errors from a taxonomy + framework defaults
|
|
817
|
+
// in one call (deduped — user entries win when keys overlap).
|
|
818
|
+
static fromTaxonomy(
|
|
819
|
+
taxonomy: ErrorTaxonomy,
|
|
820
|
+
config?: Omit<DocRegistryConfig, 'errors'> & { includeDefaults?: boolean }
|
|
821
|
+
): DocRegistry
|
|
664
822
|
}
|
|
665
823
|
```
|
|
666
824
|
|
|
@@ -732,6 +890,7 @@ function createClient<TScopes>(config: {
|
|
|
732
890
|
basePath: string
|
|
733
891
|
scopes: (client: ClientInstance) => TScopes
|
|
734
892
|
hooks?: ClientHooks
|
|
893
|
+
defaults?: ProcedureCallDefaults
|
|
735
894
|
}): TScopes
|
|
736
895
|
```
|
|
737
896
|
|
|
@@ -740,7 +899,8 @@ function createClient<TScopes>(config: {
|
|
|
740
899
|
- `config.adapter` — Transport adapter implementing `ClientAdapter`. Use `createFetchAdapter()` for fetch-based transport.
|
|
741
900
|
- `config.basePath` — Base URL prepended to all request paths (e.g., `'http://localhost:3000'`).
|
|
742
901
|
- `config.scopes` — Factory function that receives a raw `ClientInstance` and returns the typed scope object. The generated `create${ServiceName}Bindings` export (defaults to `createApiBindings`; pass `--service-name <Name>` to rename).
|
|
743
|
-
- `config.hooks` — Optional global hooks applied to every call.
|
|
902
|
+
- `config.hooks` — Optional global hooks applied to every call. Per-call hooks (passed via the options bag) run *after* global hooks — they stack, not replace.
|
|
903
|
+
- `config.defaults` — Optional default request options (timeout, signal, headers, basePath) applied to every call. Overridden by per-call options.
|
|
744
904
|
|
|
745
905
|
### Return Value
|
|
746
906
|
|
|
@@ -750,11 +910,51 @@ Returns the result of `config.scopes(clientInstance)` — a typed object where e
|
|
|
750
910
|
|
|
751
911
|
```typescript
|
|
752
912
|
interface ClientHooks {
|
|
753
|
-
onBeforeRequest?: (ctx:
|
|
754
|
-
onAfterResponse?: (ctx:
|
|
913
|
+
onBeforeRequest?: (ctx: BeforeRequestContext) => BeforeRequestContext | Promise<BeforeRequestContext>
|
|
914
|
+
onAfterResponse?: (ctx: AfterResponseContext) => void | Promise<void>
|
|
915
|
+
onError?: (ctx: ErrorContext) => void | Promise<void>
|
|
755
916
|
}
|
|
756
917
|
```
|
|
757
918
|
|
|
919
|
+
### ProcedureCallDefaults / ProcedureCallOptions
|
|
920
|
+
|
|
921
|
+
`ProcedureCallDefaults` is the shape used at `config.defaults` and is a strict subset of `ProcedureCallOptions` (per-call options). `ProcedureCallOptions` additionally includes the `ClientHooks` fields so a single options bag covers both request config and hooks.
|
|
922
|
+
|
|
923
|
+
```typescript
|
|
924
|
+
interface ProcedureCallDefaults {
|
|
925
|
+
signal?: AbortSignal // cancel signal (combined with per-call via AbortSignal.any)
|
|
926
|
+
timeout?: number // ms — per-call timeout:0 disables an inherited default
|
|
927
|
+
headers?: Record<string, string> // merged (per-call keys win)
|
|
928
|
+
basePath?: string // per-call > default > config.basePath
|
|
929
|
+
meta?: RequestMeta // typed per-request metadata (see RequestMeta below)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
interface ProcedureCallOptions extends ProcedureCallDefaults, ClientHooks {}
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
### RequestMeta (typed per-request metadata)
|
|
936
|
+
|
|
937
|
+
`RequestMeta` is an empty interface designed for TypeScript declaration merging. Augment it in your project to type the `meta` field end-to-end (per-call options, hook contexts, and the adapter).
|
|
938
|
+
|
|
939
|
+
```typescript
|
|
940
|
+
// Self-contained (code-generated) client
|
|
941
|
+
declare module './generated/_types' {
|
|
942
|
+
interface RequestMeta {
|
|
943
|
+
traceId: string
|
|
944
|
+
priority?: 'high' | 'low'
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Or when using ts-procedures/client directly
|
|
949
|
+
declare module 'ts-procedures/client' {
|
|
950
|
+
interface RequestMeta {
|
|
951
|
+
traceId: string
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
After augmentation, `request.meta` is typed in adapters, hooks, defaults, and per-call options. If `RequestMeta` declares required fields, supply them via `defaults.meta` or per-call `options.meta` — the merged shape must satisfy them at runtime.
|
|
957
|
+
|
|
758
958
|
### Example
|
|
759
959
|
|
|
760
960
|
```typescript
|
|
@@ -765,6 +965,7 @@ const client = createClient({
|
|
|
765
965
|
adapter: createFetchAdapter(),
|
|
766
966
|
basePath: 'http://localhost:3000',
|
|
767
967
|
scopes: createApiBindings,
|
|
968
|
+
defaults: { timeout: 30_000, headers: { 'X-Client-Version': '1.0.0' } },
|
|
768
969
|
hooks: {
|
|
769
970
|
onBeforeRequest(ctx) {
|
|
770
971
|
ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
|
|
@@ -773,7 +974,17 @@ const client = createClient({
|
|
|
773
974
|
},
|
|
774
975
|
})
|
|
775
976
|
|
|
776
|
-
|
|
977
|
+
// Per-call timeout + signal + hooks — all in one options bag
|
|
978
|
+
const controller = new AbortController()
|
|
979
|
+
const user = await client.users.GetUser(
|
|
980
|
+
{ pathParams: { id: '123' } },
|
|
981
|
+
{
|
|
982
|
+
timeout: 5000,
|
|
983
|
+
signal: controller.signal,
|
|
984
|
+
headers: { 'X-Request-Id': crypto.randomUUID() },
|
|
985
|
+
onAfterResponse(ctx) { log(ctx) },
|
|
986
|
+
},
|
|
987
|
+
)
|
|
777
988
|
|
|
778
989
|
// Reach types via the namespace: Api.Users.GetUser.Params, Api.Errors.ProcedureError
|
|
779
990
|
```
|