ts-procedures 5.16.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.
Files changed (146) 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 +85 -17
  5. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +163 -5
  6. package/agent_config/claude-code/skills/ts-procedures/patterns.md +169 -13
  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 +77 -12
  16. package/agent_config/cursor/cursorrules +77 -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 +21 -0
  61. package/build/implementations/http/doc-registry.js +51 -78
  62. package/build/implementations/http/doc-registry.js.map +1 -1
  63. package/build/implementations/http/doc-registry.test.js +8 -6
  64. package/build/implementations/http/doc-registry.test.js.map +1 -1
  65. package/build/implementations/http/error-taxonomy.d.ts +240 -0
  66. package/build/implementations/http/error-taxonomy.js +230 -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 +140 -0
  102. package/build/implementations/http/route-errors.test.js.map +1 -0
  103. package/build/implementations/types.d.ts +30 -2
  104. package/docs/client-and-codegen.md +105 -12
  105. package/docs/core.md +14 -5
  106. package/docs/http-integrations.md +135 -4
  107. package/docs/streaming.md +3 -1
  108. package/package.json +7 -2
  109. package/src/client/call.ts +10 -1
  110. package/src/client/error-dispatch.test.ts +72 -0
  111. package/src/client/error-dispatch.ts +27 -0
  112. package/src/client/fetch-adapter.ts +11 -5
  113. package/src/client/index.ts +9 -0
  114. package/src/client/stream.ts +14 -3
  115. package/src/client/typed-error-dispatch.test.ts +211 -0
  116. package/src/client/types.ts +42 -0
  117. package/src/codegen/e2e.test.ts +9 -4
  118. package/src/codegen/emit-client-runtime.ts +4 -0
  119. package/src/codegen/emit-errors.integration.test.ts +183 -0
  120. package/src/codegen/emit-errors.test.ts +91 -87
  121. package/src/codegen/emit-errors.ts +123 -41
  122. package/src/codegen/emit-index.test.ts +68 -24
  123. package/src/codegen/emit-index.ts +66 -4
  124. package/src/codegen/emit-scope.ts +124 -7
  125. package/src/codegen/pipeline.ts +25 -2
  126. package/src/implementations/http/README.md +19 -4
  127. package/src/implementations/http/doc-registry.test.ts +10 -6
  128. package/src/implementations/http/doc-registry.ts +63 -80
  129. package/src/implementations/http/error-taxonomy.test.ts +438 -0
  130. package/src/implementations/http/error-taxonomy.ts +337 -0
  131. package/src/implementations/http/express-rpc/README.md +21 -22
  132. package/src/implementations/http/express-rpc/error-taxonomy.test.ts +103 -0
  133. package/src/implementations/http/express-rpc/index.ts +75 -14
  134. package/src/implementations/http/hono-api/README.md +284 -0
  135. package/src/implementations/http/hono-api/error-taxonomy.test.ts +179 -0
  136. package/src/implementations/http/hono-api/index.ts +76 -1
  137. package/src/implementations/http/hono-rpc/README.md +18 -19
  138. package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +82 -0
  139. package/src/implementations/http/hono-rpc/index.ts +65 -9
  140. package/src/implementations/http/hono-stream/README.md +44 -25
  141. package/src/implementations/http/hono-stream/error-taxonomy.test.ts +98 -0
  142. package/src/implementations/http/hono-stream/index.test.ts +54 -18
  143. package/src/implementations/http/hono-stream/index.ts +83 -13
  144. package/src/implementations/http/on-request-error.test.ts +201 -0
  145. package/src/implementations/http/route-errors.test.ts +177 -0
  146. package/src/implementations/types.ts +30 -2
@@ -0,0 +1,140 @@
1
+ /* eslint-disable @typescript-eslint/no-empty-object-type */
2
+ import { describe, expect, test } from 'vitest';
3
+ import { Type } from 'typebox';
4
+ import { Procedures } from '../../index.js';
5
+ import { HonoAPIAppBuilder } from './hono-api/index.js';
6
+ import { HonoRPCAppBuilder } from './hono-rpc/index.js';
7
+ import { ExpressRPCAppBuilder } from './express-rpc/index.js';
8
+ import { HonoStreamAppBuilder } from './hono-stream/index.js';
9
+ import { DocRegistry } from './doc-registry.js';
10
+ import { defineErrorTaxonomy } from './error-taxonomy.js';
11
+ class UseCaseError extends Error {
12
+ externalMsg;
13
+ constructor(externalMsg) {
14
+ super(externalMsg);
15
+ this.externalMsg = externalMsg;
16
+ this.name = 'UseCaseError';
17
+ Object.setPrototypeOf(this, UseCaseError.prototype);
18
+ }
19
+ }
20
+ class AuthError extends Error {
21
+ }
22
+ const appErrors = defineErrorTaxonomy({
23
+ UseCaseError: { class: UseCaseError, statusCode: 422 },
24
+ AuthError: { class: AuthError, statusCode: 401 },
25
+ });
26
+ describe('per-route errors declaration', () => {
27
+ test('hono-api copies config.errors onto the route doc', () => {
28
+ const API = Procedures();
29
+ API.Create('GetUser', {
30
+ path: '/users/:id',
31
+ method: 'get',
32
+ errors: ['UseCaseError', 'AuthError'],
33
+ schema: {
34
+ input: { pathParams: Type.Object({ id: Type.String() }) },
35
+ returnType: Type.Object({}),
36
+ },
37
+ }, async () => ({}));
38
+ const builder = new HonoAPIAppBuilder().register(API, () => ({}));
39
+ builder.build();
40
+ expect(builder.docs[0].errors).toEqual(['UseCaseError', 'AuthError']);
41
+ });
42
+ test('hono-api route doc omits errors when not declared', () => {
43
+ const API = Procedures();
44
+ API.Create('Health', { path: '/health', method: 'get', schema: { returnType: Type.Object({}) } }, async () => ({}));
45
+ const builder = new HonoAPIAppBuilder().register(API, () => ({}));
46
+ builder.build();
47
+ expect(builder.docs[0].errors).toBeUndefined();
48
+ });
49
+ test('hono-rpc copies config.errors onto the route doc', () => {
50
+ const RPC = Procedures();
51
+ RPC.Create('DoThing', {
52
+ scope: 'things',
53
+ version: 1,
54
+ errors: ['UseCaseError'],
55
+ schema: { params: Type.Object({}) },
56
+ }, async () => ({}));
57
+ const builder = new HonoRPCAppBuilder().register(RPC, () => ({}));
58
+ builder.build();
59
+ expect(builder.docs[0].errors).toEqual(['UseCaseError']);
60
+ });
61
+ test('express-rpc copies config.errors onto the route doc', () => {
62
+ const RPC = Procedures();
63
+ RPC.Create('DoThing', {
64
+ scope: 'things',
65
+ version: 1,
66
+ errors: ['AuthError'],
67
+ schema: { params: Type.Object({}) },
68
+ }, async () => ({}));
69
+ const builder = new ExpressRPCAppBuilder().register(RPC, () => ({}));
70
+ builder.build();
71
+ expect(builder.docs[0].errors).toEqual(['AuthError']);
72
+ });
73
+ test('hono-stream copies config.errors onto the route doc', () => {
74
+ const Stream = Procedures();
75
+ Stream.CreateStream('Watch', { scope: 'watch', version: 1, errors: ['AuthError'] }, async function* () {
76
+ yield { ok: true };
77
+ });
78
+ const builder = new HonoStreamAppBuilder().register(Stream, () => ({}));
79
+ builder.build();
80
+ expect(builder.docs[0].errors).toEqual(['AuthError']);
81
+ });
82
+ test('compile-time: errors typed as keyof taxonomy narrows valid keys', () => {
83
+ const _valid = ['UseCaseError', 'AuthError'];
84
+ // @ts-expect-error - 'NotRegistered' is not in the taxonomy
85
+ const _invalid = ['NotRegistered'];
86
+ expect(_valid).toBeDefined();
87
+ expect(_invalid).toBeDefined();
88
+ });
89
+ });
90
+ describe('DocRegistry.fromTaxonomy', () => {
91
+ test('seeds envelope errors from the taxonomy + framework defaults', () => {
92
+ const registry = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' });
93
+ const envelope = registry.toJSON();
94
+ const names = envelope.errors.map((e) => e.name);
95
+ expect(names).toContain('UseCaseError');
96
+ expect(names).toContain('AuthError');
97
+ expect(names).toContain('ProcedureValidationError');
98
+ expect(names).toContain('ProcedureRegistrationError');
99
+ expect(envelope.basePath).toBe('/api');
100
+ });
101
+ test('includeDefaults: false omits framework entries', () => {
102
+ const registry = DocRegistry.fromTaxonomy(appErrors, { includeDefaults: false });
103
+ const names = registry.toJSON().errors.map((e) => e.name);
104
+ expect(names).toEqual(['UseCaseError', 'AuthError']);
105
+ });
106
+ test('user entry with same key as default takes precedence (deduped)', () => {
107
+ const overridden = defineErrorTaxonomy({
108
+ ProcedureError: {
109
+ class: Error,
110
+ statusCode: 418,
111
+ description: 'custom override',
112
+ },
113
+ });
114
+ const envelope = DocRegistry.fromTaxonomy(overridden).toJSON();
115
+ const proc = envelope.errors.find((e) => e.name === 'ProcedureError');
116
+ expect(proc?.statusCode).toBe(418);
117
+ expect(proc?.description).toBe('custom override');
118
+ // no duplicates
119
+ const count = envelope.errors.filter((e) => e.name === 'ProcedureError').length;
120
+ expect(count).toBe(1);
121
+ });
122
+ test('registered route errors survive DocRegistry composition', () => {
123
+ const API = Procedures();
124
+ API.Create('GetUser', {
125
+ path: '/users/:id',
126
+ method: 'get',
127
+ errors: ['UseCaseError'],
128
+ schema: {
129
+ input: { pathParams: Type.Object({ id: Type.String() }) },
130
+ returnType: Type.Object({}),
131
+ },
132
+ }, async () => ({}));
133
+ const app = new HonoAPIAppBuilder().register(API, () => ({}));
134
+ app.build();
135
+ const envelope = DocRegistry.fromTaxonomy(appErrors).from(app).toJSON();
136
+ const route = envelope.routes.find((r) => r.kind === 'api' && r.name === 'GetUser');
137
+ expect(route?.errors).toEqual(['UseCaseError']);
138
+ });
139
+ });
140
+ //# sourceMappingURL=route-errors.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-errors.test.js","sourceRoot":"","sources":["../../../src/implementations/http/route-errors.test.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,18 @@
1
1
  import { Procedures } from '../index.js';
2
- export interface RPCConfig {
2
+ /**
3
+ * @typeParam TErrorKey - Union of valid taxonomy keys. Defaults to `string`
4
+ * (unconstrained). Narrow it by passing `keyof typeof yourTaxonomy & string`
5
+ * to get compile-time typo protection on `errors`.
6
+ */
7
+ export interface RPCConfig<TErrorKey extends string = string> {
3
8
  scope: string | string[];
4
9
  version: number;
10
+ /**
11
+ * Taxonomy keys for errors this procedure may emit. Purely informational at
12
+ * runtime (nothing is rejected), but populates the DocEnvelope per-route
13
+ * error list so generated clients can narrow their catch types.
14
+ */
15
+ errors?: TErrorKey[];
5
16
  }
6
17
  export type FactoryItem<C> = {
7
18
  factory: ReturnType<typeof Procedures<C, RPCConfig>>;
@@ -16,10 +27,17 @@ export interface RPCHttpRouteDoc extends RPCConfig {
16
27
  body?: Record<string, unknown>;
17
28
  response?: Record<string, unknown>;
18
29
  };
30
+ /** Taxonomy keys for errors this route may emit. */
31
+ errors?: string[];
19
32
  }
20
33
  export type StreamMode = 'sse' | 'text';
21
34
  export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head';
22
- export interface APIConfig {
35
+ /**
36
+ * @typeParam TErrorKey - Union of valid taxonomy keys. Defaults to `string`.
37
+ * Narrow it by passing `keyof typeof yourTaxonomy & string` for compile-time
38
+ * typo protection on `errors`.
39
+ */
40
+ export interface APIConfig<TErrorKey extends string = string> {
23
41
  /** HTTP route path (supports Hono path params, e.g., '/users/:id') */
24
42
  path: string;
25
43
  /** HTTP method for this endpoint */
@@ -28,6 +46,12 @@ export interface APIConfig {
28
46
  successStatus?: number;
29
47
  /** Optional scope for grouping API routes in generated client files */
30
48
  scope?: string;
49
+ /**
50
+ * Taxonomy keys for errors this procedure may emit. Purely informational at
51
+ * runtime (nothing is rejected), but populates the DocEnvelope per-route
52
+ * error list so generated clients can narrow their catch types.
53
+ */
54
+ errors?: TErrorKey[];
31
55
  }
32
56
  export interface APIHttpRouteDoc extends APIConfig {
33
57
  kind: 'api';
@@ -41,6 +65,8 @@ export interface APIHttpRouteDoc extends APIConfig {
41
65
  headers?: Record<string, unknown>;
42
66
  response?: Record<string, unknown>;
43
67
  };
68
+ /** Taxonomy keys for errors this route may emit. */
69
+ errors?: string[];
44
70
  }
45
71
  /**
46
72
  * Constrains schema.input channel names to valid HTTP input sources.
@@ -76,6 +102,8 @@ export interface StreamHttpRouteDoc extends RPCConfig {
76
102
  yieldType?: Record<string, unknown>;
77
103
  returnType?: Record<string, unknown>;
78
104
  };
105
+ /** Taxonomy keys for errors this route may emit (pre-stream only). */
106
+ errors?: string[];
79
107
  }
80
108
  /**
81
109
  * Extracts the TContext type from a Procedures factory return type.
@@ -20,14 +20,14 @@ npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated
20
20
 
21
21
  **Step 3 — Use the client:**
22
22
 
23
+ The recommended entrypoint is the generated `create${Service}Client` factory. It calls `createClient` under the hood with the generated error registry pre-wired, so non-2xx responses arrive as typed class instances you can catch with `instanceof`.
24
+
23
25
  ```typescript
24
- import { createClient, createFetchAdapter } from 'ts-procedures/client'
25
- import { createApiBindings, Api } from './generated/api'
26
+ import { createApiClient, ApiErrors, createFetchAdapter } from './generated/api'
26
27
 
27
- const client = createClient({
28
+ const api = createApiClient({
28
29
  adapter: createFetchAdapter(),
29
30
  basePath: 'http://localhost:3000',
30
- scopes: createApiBindings,
31
31
  hooks: {
32
32
  onBeforeRequest(ctx) {
33
33
  ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
@@ -36,11 +36,32 @@ const client = createClient({
36
36
  },
37
37
  })
38
38
 
39
- // Fully typed — params and response inferred from server schemas
40
- const user = await client.users.GetUser({ pathParams: { id: '123' } })
39
+ try {
40
+ const user = await api.users.GetUser({ pathParams: { id: '123' } })
41
+ } catch (err) {
42
+ if (err instanceof ApiErrors.UseCaseError) {
43
+ // err.body, err.status, err.procedureName, err.scope all typed
44
+ } else if (err instanceof ApiErrors.ApiProcedureError) {
45
+ // Catch-all for any service error (shared base class)
46
+ }
47
+ }
41
48
 
42
- // Types live under the service namespace: Api.Users.GetUser.Params, Api.Errors.ProcedureError, etc.
43
- // Pass --service-name <Name> to rename `Api` → `<Name>` (factory becomes create<Name>Bindings).
49
+ // Types live under the service namespace: Api.Users.GetUser.Params, Api.Errors.UseCaseError, etc.
50
+ // Pass --service-name <Name> to rename `Api` → `<Name>` throughout.
51
+ ```
52
+
53
+ For advanced control (sharing one `ClientInstance` across services, composing with a custom registry), use `createClient` + `create${Service}Bindings` directly:
54
+
55
+ ```typescript
56
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
57
+ import { createApiBindings, ApiErrors } from './generated/api'
58
+
59
+ const api = createClient({
60
+ adapter: createFetchAdapter(),
61
+ basePath: 'http://localhost:3000',
62
+ scopes: createApiBindings,
63
+ errorRegistry: ApiErrors.ApiErrorRegistry, // opt in to typed dispatch — omit to get the plain ClientRequestError transport shape
64
+ })
44
65
  ```
45
66
 
46
67
  ## Generated File Structure
@@ -54,8 +75,8 @@ generated/
54
75
  notifications.ts # Types + callables for stream procedures
55
76
  _types.ts # Client type definitions (self-contained mode)
56
77
  _client.ts # Client runtime bundle (self-contained mode)
57
- _errors.ts # Typed error classes + ${ServiceName}ProcedureErrorUnion (default: ApiProcedureErrorUnion)
58
- index.ts # Service namespace (`export namespace ${ServiceName}`) + create${ServiceName}Bindings factory (default: Api / createApiBindings)
78
+ _errors.ts # Runtime error classes extending ${ServiceName}ProcedureError + static fromResponse + ${ServiceName}ErrorRegistry + ${ServiceName}ProcedureErrorUnion (default: ApiProcedureError / ApiErrorRegistry / ApiProcedureErrorUnion)
79
+ index.ts # Service namespace + create${ServiceName}Bindings (manual wiring) and create${ServiceName}Client (convenience; registry pre-wired). Default: Api / createApiBindings / createApiClient.
59
80
  ```
60
81
 
61
82
  ## CLI Reference
@@ -217,6 +238,68 @@ await client.users.GetUser(
217
238
 
218
239
  If `RequestMeta` declares required fields, the merged meta (defaults + per-call) must contain them at runtime. TypeScript can't statically verify this across the merge boundary, so supply required fields in `defaults.meta` or per-call `options.meta`.
219
240
 
241
+ ## Typed Error Handling
242
+
243
+ Non-2xx responses whose body has a `name` field matching a registry key are dispatched into typed class instances. The generated `_errors.ts` exports:
244
+
245
+ - A shared base class `${ServiceName}ProcedureError<TBody>` — carries `status`, `procedureName`, `scope`, and `body`. Catch any service error with `instanceof ${ServiceName}Errors.${ServiceName}ProcedureError`.
246
+ - One concrete class per taxonomy entry in the envelope, each with a static `fromResponse(body, meta)` factory.
247
+ - `${ServiceName}ErrorRegistry` — a `{ [name]: ErrorClass }` object consumed by the client's `dispatchTypedError`.
248
+ - `${ServiceName}ProcedureErrorUnion` — a type union of every generated class for annotating catch clauses.
249
+
250
+ ```typescript
251
+ import { createApiClient, ApiErrors } from './generated/api'
252
+
253
+ const api = createApiClient({ adapter, basePath: '...' })
254
+
255
+ try {
256
+ await api.users.GetUser({ pathParams: { id: 'x' } })
257
+ } catch (err) {
258
+ if (err instanceof ApiErrors.UseCaseError) {
259
+ // err.body is typed to UseCaseErrorBody; err.status, err.procedureName, err.scope available
260
+ } else if (err instanceof ApiErrors.ApiProcedureError) {
261
+ // Catch-all for any generated service error
262
+ } else if (err instanceof ClientRequestError) {
263
+ // Transport-level error (unmatched body.name, or non-JSON response)
264
+ }
265
+ }
266
+ ```
267
+
268
+ ### Per-route error narrowing
269
+
270
+ When a server route declares `errors: ['UseCaseError', 'AuthError']` on its config, the generated scope file emits an `Errors` type union inside that route's namespace:
271
+
272
+ ```typescript
273
+ // Generated scope file:
274
+ export namespace Users {
275
+ export namespace GetUser {
276
+ export type Params = { /* ... */ }
277
+ export type Response = { /* ... */ }
278
+ export type Errors = ApiErrors.UseCaseError | ApiErrors.AuthError
279
+ }
280
+ }
281
+
282
+ // Consumer:
283
+ try { await api.users.GetUser({ pathParams: { id: 'x' } }) }
284
+ catch (err) {
285
+ const e = err as Users.GetUser.Errors | ClientRequestError // TS has no typed-throws; manual annotation
286
+ if (e instanceof ApiErrors.UseCaseError) { /* ... */ }
287
+ }
288
+ ```
289
+
290
+ Route-errors that reference keys missing from `_errors.ts` are filtered out at codegen time so generated code never references undefined types.
291
+
292
+ ### How dispatch works
293
+
294
+ `src/client/error-dispatch.ts` exports a pure `dispatchTypedError(registry, body, meta)` helper. The client's `call.ts` invokes it on non-2xx responses:
295
+
296
+ 1. If no registry, or body isn't an object with a string `name`, or no registry key matches → returns `null`.
297
+ 2. Otherwise calls `registry[body.name].fromResponse(body, meta)` and returns the typed `Error` instance.
298
+
299
+ When the helper returns `null`, the call falls back to `ClientRequestError` — the plain transport-error shape, used for unmatched names, non-JSON bodies, and when no registry is configured.
300
+
301
+ Stream endpoints get the same treatment for **pre-stream** errors (thrown before the first yield — e.g. validation or auth). The fetch adapter's `stream()` eagerly parses a JSON body when status is non-2xx and exposes it via `AdapterStreamResponse.errorBody`, which `executeStream` passes through the registry. **Mid-stream** SSE error events continue to flow through `onMidStreamError` on the server and re-throw on the client without registry dispatch.
302
+
220
303
  ## Hooks
221
304
 
222
305
  Hooks let you intercept requests and responses globally or per-procedure call. Global hooks apply to every call made through the client instance; per-procedure hooks run after global ones for a single invocation.
@@ -307,12 +390,22 @@ import { createClient, createFetchAdapter } from './generated/_client'
307
390
  import { createClient, createFetchAdapter } from 'ts-procedures/client'
308
391
  ```
309
392
 
393
+ - **`_client.ts`** also bundles `error-dispatch.ts`, so registry-based typed error dispatch works without importing `ts-procedures/client` at runtime.
394
+
310
395
  ## Type Exports
311
396
 
312
397
  ```typescript
313
398
  // Client Runtime
314
- import { createClient, createFetchAdapter } from 'ts-procedures/client'
315
- import type { ClientAdapter, ClientHooks, TypedStream, ClientInstance } from 'ts-procedures/client'
399
+ import { createClient, createFetchAdapter, dispatchTypedError } from 'ts-procedures/client'
400
+ import type {
401
+ ClientAdapter,
402
+ ClientHooks,
403
+ TypedStream,
404
+ ClientInstance,
405
+ ErrorRegistry,
406
+ ErrorFactory,
407
+ ErrorResponseMeta,
408
+ } from 'ts-procedures/client'
316
409
 
317
410
  // Code Generation
318
411
  import { generateClient } from 'ts-procedures/codegen'
package/docs/core.md CHANGED
@@ -31,7 +31,7 @@ Create(name, config, handler)
31
31
  **Returns:**
32
32
  - `{ [name]: handler }` - Named export for the handler
33
33
  - `procedure` - Generic reference to the handler
34
- - `info` - Procedure meta (name, description, schema, `TExtendedConfig` properties, etc.)
34
+ - `info` - Procedure metadata (name, description, schema, `TExtendedConfig` properties, etc.)
35
35
 
36
36
  ## Structured Input with schema.input
37
37
 
@@ -234,7 +234,8 @@ Create(
234
234
  async (ctx, params) => {
235
235
  const resource = await db.find(params.id)
236
236
  if (!resource) {
237
- throw ctx.error(404, 'Resource not found', { id: params.id })
237
+ // Signature: ctx.error(message: string, meta?: object) => ProcedureError
238
+ throw ctx.error('Resource not found', { code: 'NOT_FOUND', id: params.id })
238
239
  }
239
240
  return resource
240
241
  },
@@ -249,6 +250,7 @@ Create(
249
250
  | ProcedureValidationError | Schema validation failure (params) |
250
251
  | ProcedureYieldValidationError | Yield validation failure (streaming with `validateYields: true`) |
251
252
  | ProcedureRegistrationError | Invalid schema at registration |
253
+ | `${Service}ProcedureError` (client-side) | Base class for every generated error class on the client — see [Client & Codegen → Typed Error Handling](./client-and-codegen.md#typed-error-handling) |
252
254
 
253
255
  ### Error Properties
254
256
 
@@ -264,6 +266,12 @@ try {
264
266
  }
265
267
  ```
266
268
 
269
+ ### Error Taxonomy (HTTP builders)
270
+
271
+ For structured HTTP error responses, declare an error taxonomy once with `defineErrorTaxonomy` and pass it to any HTTP builder via the `errors` option — handlers `throw` their classes and the builder serializes to the configured status + body. `ctx.error()` continues to work as the ad-hoc ProcedureError path.
272
+
273
+ The taxonomy also drives typed `catch` blocks on generated clients and per-route error documentation. See [HTTP Integrations → Error Handling](./http-integrations.md#error-handling) for the full contract, and [Client & Codegen → Typed Error Handling](./client-and-codegen.md#typed-error-handling) for the client-side surface.
274
+
267
275
  ## Abort Signal
268
276
 
269
277
  For regular procedures, `ctx.signal` is available when the server implementation provides it. The built-in HTTP integrations (Hono RPC, Express RPC) inject the request's abort signal automatically:
@@ -394,10 +402,11 @@ describe('GetUser', () => {
394
402
  })
395
403
 
396
404
  test('has correct schema', () => {
405
+ // `info.schema.params` is the JSON Schema derived from the TypeBox
406
+ // definition above: `Type.Object({ hideName: Type.Optional(Type.Boolean()) })`.
397
407
  expect(info.schema.params).toEqual({
398
408
  type: 'object',
399
- properties: { id: { type: 'string' } },
400
- required: ['id'],
409
+ properties: { hideName: { type: 'boolean' } },
401
410
  })
402
411
  })
403
412
  })
@@ -431,7 +440,7 @@ Defines a procedure.
431
440
  **Returns:**
432
441
  - `{ [name]: handler }` - Named handler export
433
442
  - `procedure` - Generic handler reference
434
- - `info` - Procedure metareturnType
443
+ - `info` - Procedure metadata (name, description, schema, extended-config properties)
435
444
 
436
445
  ### Type Exports
437
446
 
@@ -13,7 +13,7 @@ For a full cross-framework comparison (config interfaces, path generation, conte
13
13
  | Express RPC | `ts-procedures/express-rpc` | RPC (POST routes) | [README](../src/implementations/http/express-rpc/README.md) |
14
14
  | Hono RPC | `ts-procedures/hono-rpc` | RPC (POST routes) | [README](../src/implementations/http/hono-rpc/README.md) |
15
15
  | Hono Stream | `ts-procedures/hono-stream` | SSE/text streaming | [README](../src/implementations/http/hono-stream/README.md) |
16
- | Hono API | `ts-procedures/hono-api` | REST (method-based) | [Below](#hono-api-integration) |
16
+ | Hono API | `ts-procedures/hono-api` | REST (method-based) | [README](../src/implementations/http/hono-api/README.md) |
17
17
 
18
18
  ## Express RPC
19
19
 
@@ -27,7 +27,7 @@ const RPC = Procedures<AppContext, RPCConfig>()
27
27
  RPC.Create(
28
28
  'GetUser',
29
29
  {
30
- name: ['users', 'get'],
30
+ scope: ['users', 'get'],
31
31
  version: 1,
32
32
  schema: {
33
33
  params: Type.Object({ id: Type.String() }),
@@ -44,7 +44,7 @@ const app = new ExpressRPCAppBuilder()
44
44
  .build()
45
45
 
46
46
  app.listen(3000)
47
- // Route created: POST /rpc/users/get/1
47
+ // Route created: POST /users/get/get-user/1
48
48
  ```
49
49
 
50
50
  See the [Express RPC Integration Guide](../src/implementations/http/express-rpc/README.md) for complete setup including lifecycle hooks, error handling, and route documentation.
@@ -139,7 +139,7 @@ API.Create('CreateUser', {
139
139
  return await createUser(body)
140
140
  })
141
141
 
142
- const app = await new HonoAPIAppBuilder({ pathPrefix: '/api' })
142
+ const app = new HonoAPIAppBuilder({ pathPrefix: '/api' })
143
143
  .register(API, (c) => ({ userId: c.req.header('x-user-id') || 'anonymous' }))
144
144
  .build()
145
145
 
@@ -148,6 +148,129 @@ const app = await new HonoAPIAppBuilder({ pathPrefix: '/api' })
148
148
  // POST /api/users -> 201
149
149
  ```
150
150
 
151
+ See the [Hono API Integration Guide](../src/implementations/http/hono-api/README.md) for the full surface: `schema.input` channels, query parsing, success-status defaults, and error handling.
152
+
153
+ ## Error Handling
154
+
155
+ Every HTTP builder supports **two peer error-handling modes** — neither is "primary" or "fallback". Pick whichever fits your app, or combine them.
156
+
157
+ | Mode | When to pick | What you configure |
158
+ |---|---|---|
159
+ | **Declarative — the taxonomy** | Structured errors, typed client dispatch, DocEnvelope integration | `errors` (taxonomy) + optional `unknownError` |
160
+ | **Imperative — the `onError` callback** | Simple apps, gradual migration, full response control, no typed client contract | `onError` only |
161
+
162
+ Both modes also expose `onRequestError` — a cross-cutting observer for logging, tracing, and metrics that fires for every error regardless of dispatch outcome.
163
+
164
+ ### Declarative — the taxonomy
165
+
166
+ ```typescript
167
+ import { defineErrorTaxonomy } from 'ts-procedures/http-errors'
168
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
169
+
170
+ class UseCaseError extends Error {
171
+ constructor(readonly externalMsg: string, readonly internalMsg: string) {
172
+ super(externalMsg); this.name = 'UseCaseError'
173
+ Object.setPrototypeOf(this, UseCaseError.prototype)
174
+ }
175
+ }
176
+
177
+ const appErrors = defineErrorTaxonomy({
178
+ UseCaseError: {
179
+ class: UseCaseError,
180
+ statusCode: 422,
181
+ toResponse: (err) => ({ message: err.externalMsg }), // `name: 'UseCaseError'` auto-injected
182
+ onCatch: (err) => logger.error(err.internalMsg),
183
+ },
184
+ // 3rd-party errors without a subclassable type use `match:` instead of `class:`.
185
+ MongoDuplicateKey: {
186
+ match: (err): err is Error =>
187
+ err instanceof Error && err.name === 'MongoServerError' && (err as any).code === 11000,
188
+ statusCode: 409,
189
+ toResponse: () => ({ message: 'Resource already exists' }),
190
+ },
191
+ })
192
+
193
+ new HonoAPIAppBuilder({
194
+ errors: appErrors,
195
+ unknownError: {
196
+ statusCode: 500,
197
+ toResponse: () => ({ name: 'InternalServerError', message: 'Unexpected error' }),
198
+ onCatch: (err, { procedure }) => logger.error({ procedure: procedure.name, err }),
199
+ },
200
+ }).register(API, /* ... */).build()
201
+ ```
202
+
203
+ The identical `errors` + `unknownError` shape plugs into `HonoRPCAppBuilder`, `ExpressRPCAppBuilder`, and `HonoStreamAppBuilder` (pre-stream only — mid-stream uses `onMidStreamError`).
204
+
205
+ ### Imperative — the `onError` callback
206
+
207
+ For apps that don't need typed client dispatch or declarative docs, configure `onError` directly and handle every error in one place:
208
+
209
+ ```typescript
210
+ new HonoAPIAppBuilder({
211
+ onError: (procedure, c, error) => {
212
+ logger.error(`[${procedure.name}]`, error)
213
+ return c.json({ error: error.message ?? 'unknown error' }, 500)
214
+ },
215
+ }).register(API, /* ... */).build()
216
+ ```
217
+
218
+ (Need to route different error classes to different status codes? Use the taxonomy — that's exactly what it's designed for. Hand-writing `instanceof` ladders inside `onError` is [anti-pattern #20](../agent_config/claude-code/skills/ts-procedures/anti-patterns.md).)
219
+
220
+ Signatures (differ by framework):
221
+
222
+ | Builder | `onError` signature |
223
+ |---|---|
224
+ | `HonoAPIAppBuilder`, `HonoRPCAppBuilder` | `(procedure, c: Context, error) => Response \| Promise<Response>` |
225
+ | `ExpressRPCAppBuilder` | `(procedure, req, res, error) => void` (write to `res`) |
226
+ | `HonoStreamAppBuilder` | `(procedure, c: Context, error) => Response \| Promise<Response>` — pre-stream only |
227
+
228
+ Picking between modes isn't irreversible: you can start with `onError`, migrate chunks to the taxonomy as your error model stabilizes, and leave the rest in `onError`. Both modes coexist in the dispatch order below.
229
+
230
+ ### Per-route error narrowing (taxonomy mode)
231
+
232
+ `APIConfig` and `RPCConfig` are generic over a `TErrorKey extends string` parameter so you can declare which errors a specific route may emit:
233
+
234
+ ```typescript
235
+ import type { APIConfig } from 'ts-procedures/http'
236
+
237
+ type MyAPIConfig = APIConfig<keyof typeof appErrors & string>
238
+ const API = Procedures<Ctx, MyAPIConfig>()
239
+
240
+ API.Create('GetUser', {
241
+ path: '/users/:id',
242
+ method: 'get',
243
+ errors: ['UseCaseError'], // typo-checked against the taxonomy keys
244
+ schema: { /* ... */ },
245
+ }, /* handler */)
246
+ ```
247
+
248
+ These per-route declarations flow into the DocEnvelope and drive typed `catch` blocks on generated clients — see [Client & Codegen → Typed Error Handling](./client-and-codegen.md#typed-error-handling).
249
+
250
+ ### Cross-cutting observability — `onRequestError`
251
+
252
+ `onRequestError` fires for every caught error, **before** dispatch. It's an observer — it can't mutate the response. Use it for APM, distributed tracing, custom logging, or metrics where you want one hook that sees every error regardless of which mode dispatched it.
253
+
254
+ ```typescript
255
+ new HonoAPIAppBuilder({
256
+ errors: appErrors,
257
+ onRequestError: async ({ err, procedure, raw }) => {
258
+ sentry.captureException(err, { procedure: procedure.name })
259
+ },
260
+ })
261
+ ```
262
+
263
+ The observer is awaited before the response is sent, and any error it throws is swallowed (logged to the console) so a broken instrumentation hook can't corrupt the primary flow.
264
+
265
+ ### Dispatch order inside each builder's catch block
266
+
267
+ 1. **`onRequestError`** (observer) — awaited, can't alter dispatch or response
268
+ 2. **`errors` taxonomy** — user entries checked, then framework defaults, then `unknownError`
269
+ 3. **`onError` callback** — imperative handler receives anything the taxonomy didn't match
270
+ 4. **Hard default** — `{ error: message }` at status 500 (produced only when nothing above handled the error)
271
+
272
+ Configuring only the taxonomy, only `onError`, both, or neither are all valid. When neither is configured the builder goes straight from step 1 to step 4.
273
+
151
274
  ## DocRegistry — Composing Docs from Multiple Builders
152
275
 
153
276
  Use `DocRegistry` to compose route documentation from any combination of HTTP builders into a typed envelope:
@@ -169,6 +292,14 @@ app.get('/docs', (c) => c.json(docs.toJSON()))
169
292
 
170
293
  `from()` stores a reference — routes are read lazily at `toJSON()` time, so builders can be registered before or after `.build()`. Supports optional `filter` and `transform` options for customizing output.
171
294
 
295
+ `DocRegistry.fromTaxonomy(taxonomy, config?)` is a convenience constructor that seeds `envelope.errors` from your taxonomy plus framework defaults in one call (deduped — your entries win when keys overlap):
296
+
297
+ ```typescript
298
+ const docs = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
299
+ .from(rpcBuilder)
300
+ .from(apiBuilder)
301
+ ```
302
+
172
303
  The `DocRegistry` output is the input for [Client Code Generation](./client-and-codegen.md).
173
304
 
174
305
  ## Type Exports
package/docs/streaming.md CHANGED
@@ -168,7 +168,9 @@ For the built-in Hono streaming integration, see the [Hono Stream README](../src
168
168
 
169
169
  ## Stream Errors
170
170
 
171
- Streaming procedures support the same error handling as regular procedures (see [Error Handling](./core.md#error-handling)):
171
+ Streaming procedures support the same error handling as regular procedures (see [Error Handling](./core.md#error-handling)).
172
+
173
+ For HTTP stream endpoints, pre-stream errors (validation, context resolution, anything thrown before the first yield) go through the same two peer error-handling modes as every other HTTP builder — either the declarative `errors` / `unknownError` taxonomy (from `defineErrorTaxonomy`) or the imperative `onError` callback on `HonoStreamAppBuilder`. Cross-cutting `onRequestError` observer fires for every pre-stream error. See [HTTP Integrations → Error Handling](./http-integrations.md#error-handling) for the full contract. Mid-stream errors (thrown after the first yield) still flow through `onMidStreamError` and are surfaced to the client as SSE error events — the HTTP status is already committed once streaming begins, so mid-stream is outside the peer error modes.
172
174
 
173
175
  ```typescript
174
176
  const { StreamWithErrors } = CreateStream(