ts-procedures 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -0
- package/agent_config/bin/postinstall.mjs +105 -0
- package/agent_config/bin/setup.mjs +286 -0
- package/agent_config/claude-code/.claude-plugin/plugin.json +5 -0
- package/agent_config/claude-code/agents/ts-procedures-architect.md +188 -0
- package/agent_config/claude-code/skills/guide/SKILL.md +142 -0
- package/agent_config/claude-code/skills/guide/anti-patterns.md +608 -0
- package/agent_config/claude-code/skills/guide/api-reference.md +696 -0
- package/agent_config/claude-code/skills/guide/patterns.md +727 -0
- package/agent_config/claude-code/skills/review/SKILL.md +53 -0
- package/agent_config/claude-code/skills/review/checklist.md +163 -0
- package/agent_config/claude-code/skills/scaffold/SKILL.md +56 -0
- package/agent_config/claude-code/skills/scaffold/templates/express-rpc.md +134 -0
- package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +169 -0
- package/agent_config/claude-code/skills/scaffold/templates/hono-rpc.md +139 -0
- package/agent_config/claude-code/skills/scaffold/templates/hono-stream.md +134 -0
- package/agent_config/claude-code/skills/scaffold/templates/procedure.md +77 -0
- package/agent_config/claude-code/skills/scaffold/templates/stream-procedure.md +113 -0
- package/agent_config/copilot/copilot-instructions.md +290 -0
- package/agent_config/cursor/cursorrules +290 -0
- package/agent_config/lib/install-claude.mjs +109 -0
- package/build/implementations/http/hono-api/index.d.ts +102 -0
- package/build/implementations/http/hono-api/index.js +339 -0
- package/build/implementations/http/hono-api/index.js.map +1 -0
- package/build/implementations/http/hono-api/index.test.d.ts +1 -0
- package/build/implementations/http/hono-api/index.test.js +983 -0
- package/build/implementations/http/hono-api/index.test.js.map +1 -0
- package/build/implementations/http/hono-api/types.d.ts +13 -0
- package/build/implementations/http/hono-api/types.js +2 -0
- package/build/implementations/http/hono-api/types.js.map +1 -0
- package/build/implementations/types.d.ts +44 -0
- package/build/index.d.ts +28 -6
- package/build/index.js +28 -0
- package/build/index.js.map +1 -1
- package/build/schema/compute-schema.d.ts +5 -0
- package/build/schema/compute-schema.js +8 -1
- package/build/schema/compute-schema.js.map +1 -1
- package/build/schema/parser.d.ts +6 -5
- package/build/schema/parser.js +54 -0
- package/build/schema/parser.js.map +1 -1
- package/package.json +14 -4
- package/src/implementations/http/README.md +45 -2
- package/src/implementations/http/hono-api/index.test.ts +1328 -0
- package/src/implementations/http/hono-api/index.ts +461 -0
- package/src/implementations/http/hono-api/types.ts +16 -0
- package/src/implementations/types.ts +52 -0
- package/src/index.ts +87 -10
- package/src/schema/compute-schema.ts +23 -2
- package/src/schema/parser.ts +70 -3
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
# ts-procedures Anti-Patterns
|
|
2
|
+
|
|
3
|
+
These are common mistakes when using ts-procedures. Each anti-pattern includes a fix.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Throwing Raw Errors Instead of ctx.error()
|
|
8
|
+
|
|
9
|
+
**Problem:** Throwing plain `Error` loses procedure metadata and makes error handling inconsistent.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// BAD
|
|
13
|
+
async (ctx, params) => {
|
|
14
|
+
if (!params.id) {
|
|
15
|
+
throw new Error('ID is required')
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Fix:** Use `ctx.error()` for business logic errors.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// GOOD
|
|
24
|
+
async (ctx, params) => {
|
|
25
|
+
if (!params.id) {
|
|
26
|
+
throw ctx.error('ID is required', { code: 'MISSING_ID' })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Why:** `ctx.error()` creates a `ProcedureError` with `procedureName`, `meta`, `definedAt`, and enhanced stack trace. Raw errors get wrapped but lose the `meta` field and intentional error semantics.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 2. Putting Validation Logic in the Handler
|
|
36
|
+
|
|
37
|
+
**Problem:** Manually validating params when schema validation handles it automatically.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// BAD
|
|
41
|
+
Create('GetUser', {}, async (ctx, params) => {
|
|
42
|
+
if (!params.userId || typeof params.userId !== 'string') {
|
|
43
|
+
throw ctx.error('userId is required and must be a string')
|
|
44
|
+
}
|
|
45
|
+
return fetchUser(params.userId)
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Fix:** Use `schema.params` — AJV validates automatically.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// GOOD
|
|
53
|
+
Create('GetUser', {
|
|
54
|
+
schema: {
|
|
55
|
+
params: Type.Object({ userId: Type.String() }),
|
|
56
|
+
},
|
|
57
|
+
}, async (ctx, params) => {
|
|
58
|
+
// params.userId is guaranteed to be a string
|
|
59
|
+
return fetchUser(params.userId)
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Why:** AJV provides detailed error messages, collects all errors (`allErrors: true`), coerces types, and removes additional properties. Manual validation duplicates this and is less thorough.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 3. Expecting returnType to Be Validated at Runtime
|
|
68
|
+
|
|
69
|
+
**Problem:** Assuming `schema.returnType` validates the handler's return value.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// BAD — developer expects runtime validation of return value
|
|
73
|
+
Create('GetUser', {
|
|
74
|
+
schema: {
|
|
75
|
+
params: Type.Object({ id: Type.String() }),
|
|
76
|
+
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
77
|
+
},
|
|
78
|
+
}, async (ctx, params) => {
|
|
79
|
+
return { id: 123, wrong: 'field' } // No error thrown!
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Fix:** Understand that `returnType` is documentation only. Rely on TypeScript for return type safety.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// GOOD — TypeScript enforces return type via generic inference
|
|
87
|
+
Create('GetUser', {
|
|
88
|
+
schema: {
|
|
89
|
+
params: Type.Object({ id: Type.String() }),
|
|
90
|
+
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
91
|
+
},
|
|
92
|
+
}, async (ctx, params): Promise<{ id: string; name: string }> => {
|
|
93
|
+
return { id: params.id, name: 'John' } // TypeScript catches mismatches
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 4. Ignoring ctx.signal in Async Operations
|
|
100
|
+
|
|
101
|
+
**Problem:** Not passing `signal` to downstream calls, preventing cancellation on client disconnect.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// BAD
|
|
105
|
+
async (ctx, params) => {
|
|
106
|
+
const data = await fetch('https://api.example.com/heavy-query')
|
|
107
|
+
const processed = await heavyComputation(data)
|
|
108
|
+
return processed
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Fix:** Pass `ctx.signal` to all async calls.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// GOOD
|
|
116
|
+
async (ctx, params) => {
|
|
117
|
+
const data = await fetch('https://api.example.com/heavy-query', {
|
|
118
|
+
signal: ctx.signal,
|
|
119
|
+
})
|
|
120
|
+
const processed = await heavyComputation(data, { signal: ctx.signal })
|
|
121
|
+
return processed
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Why:** When a client disconnects, the HTTP implementation aborts the signal. Without passing it, the handler continues wasting resources on a response nobody will receive.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 5. Not Checking signal.aborted in Stream Loops
|
|
130
|
+
|
|
131
|
+
**Problem:** Stream continues producing values after client disconnects.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// BAD
|
|
135
|
+
async function* (ctx, params) {
|
|
136
|
+
while (true) {
|
|
137
|
+
const data = await pollForData()
|
|
138
|
+
yield data
|
|
139
|
+
await delay(1000)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Fix:** Check `ctx.signal.aborted` in the loop condition.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// GOOD
|
|
148
|
+
async function* (ctx, params) {
|
|
149
|
+
while (!ctx.signal.aborted) {
|
|
150
|
+
const data = await pollForData({ signal: ctx.signal })
|
|
151
|
+
yield data
|
|
152
|
+
await delay(1000)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 6. Duplicate Procedure Names
|
|
160
|
+
|
|
161
|
+
**Problem:** Registering two procedures with the same name in the same factory.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// BAD — throws ProcedureRegistrationError
|
|
165
|
+
Create('GetUser', { scope: 'users', version: 1 }, handler1)
|
|
166
|
+
Create('GetUser', { scope: 'admin', version: 1 }, handler2)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Fix:** Use unique names. Use scope/version to differentiate routes.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// GOOD
|
|
173
|
+
Create('GetUser', { scope: 'users', version: 1 }, handler1)
|
|
174
|
+
Create('GetUserAdmin', { scope: 'admin', version: 1 }, handler2)
|
|
175
|
+
|
|
176
|
+
// Or use separate factories
|
|
177
|
+
const UserRPC = Procedures<UserContext, RPCConfig>()
|
|
178
|
+
const AdminRPC = Procedures<AdminContext, RPCConfig>()
|
|
179
|
+
UserRPC.Create('GetUser', { scope: 'users', version: 1 }, handler1)
|
|
180
|
+
AdminRPC.Create('GetUser', { scope: 'admin', version: 1 }, handler2)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 7. Using validateYields Without yieldType Schema
|
|
186
|
+
|
|
187
|
+
**Problem:** Setting `validateYields: true` without providing a `yieldType` schema.
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// BAD — validateYields has no effect without yieldType schema
|
|
191
|
+
CreateStream('Stream', {
|
|
192
|
+
validateYields: true,
|
|
193
|
+
schema: {
|
|
194
|
+
params: Type.Object({ id: Type.String() }),
|
|
195
|
+
},
|
|
196
|
+
}, async function* (ctx, params) {
|
|
197
|
+
yield { anything: 'goes' }
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Fix:** Provide `yieldType` schema when using `validateYields`.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// GOOD
|
|
205
|
+
CreateStream('Stream', {
|
|
206
|
+
validateYields: true,
|
|
207
|
+
schema: {
|
|
208
|
+
params: Type.Object({ id: Type.String() }),
|
|
209
|
+
yieldType: Type.Object({ id: Type.String(), value: Type.Number() }),
|
|
210
|
+
},
|
|
211
|
+
}, async function* (ctx, params) {
|
|
212
|
+
yield { id: '1', value: 42 }
|
|
213
|
+
})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 8. Swallowing Errors in Handlers
|
|
219
|
+
|
|
220
|
+
**Problem:** Catching errors without re-throwing, hiding failures from the caller.
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// BAD
|
|
224
|
+
async (ctx, params) => {
|
|
225
|
+
try {
|
|
226
|
+
return await riskyOperation(params)
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error('Operation failed:', err)
|
|
229
|
+
return null // Caller thinks it succeeded
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Fix:** Let errors propagate. The framework wraps them in `ProcedureError` with enhanced stack traces.
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// GOOD — let the framework handle it
|
|
238
|
+
async (ctx, params) => {
|
|
239
|
+
return await riskyOperation(params)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Or rethrow as a business error with context
|
|
243
|
+
async (ctx, params) => {
|
|
244
|
+
try {
|
|
245
|
+
return await riskyOperation(params)
|
|
246
|
+
} catch (err) {
|
|
247
|
+
throw ctx.error('Operation failed', { originalError: err.message, params })
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 9. Not Using AJV's coerceTypes Behavior
|
|
255
|
+
|
|
256
|
+
**Problem:** Treating query string values as strings when AJV will coerce them.
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// BAD — unnecessary manual parsing
|
|
260
|
+
async (ctx, params) => {
|
|
261
|
+
const page = parseInt(params.page, 10)
|
|
262
|
+
const limit = parseInt(params.limit, 10)
|
|
263
|
+
return fetchPage(page, limit)
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Fix:** Let AJV coerce types via the schema.
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// GOOD — AJV coerces "5" → 5 automatically
|
|
271
|
+
Create('ListUsers', {
|
|
272
|
+
schema: {
|
|
273
|
+
params: Type.Object({
|
|
274
|
+
page: Type.Number(),
|
|
275
|
+
limit: Type.Number(),
|
|
276
|
+
}),
|
|
277
|
+
},
|
|
278
|
+
}, async (ctx, params) => {
|
|
279
|
+
// params.page and params.limit are already numbers
|
|
280
|
+
return fetchPage(params.page, params.limit)
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Why:** AJV is configured with `coerceTypes: true`. String `"5"` from query params becomes number `5` after validation.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## 10. Forgetting removeAdditional Strips Extra Fields
|
|
289
|
+
|
|
290
|
+
**Problem:** Passing extra fields in params and expecting them to survive validation.
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
// BAD — extra fields silently removed
|
|
294
|
+
const result = await GetUser(ctx, {
|
|
295
|
+
userId: '123',
|
|
296
|
+
debug: true, // Stripped by AJV
|
|
297
|
+
extraData: 'value', // Stripped by AJV
|
|
298
|
+
})
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Fix:** Only pass fields defined in the schema. If you need additional fields, add them to the schema.
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// GOOD
|
|
305
|
+
Create('GetUser', {
|
|
306
|
+
schema: {
|
|
307
|
+
params: Type.Object({
|
|
308
|
+
userId: Type.String(),
|
|
309
|
+
debug: Type.Optional(Type.Boolean()), // Include in schema if needed
|
|
310
|
+
}),
|
|
311
|
+
},
|
|
312
|
+
}, handler)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Why:** AJV is configured with `removeAdditional: true`. Any property not defined in the schema is silently removed before the handler receives params.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## 11. Creating Factory Without Context Type When Using HTTP Builders
|
|
320
|
+
|
|
321
|
+
**Problem:** Using `Procedures()` without a context type, then registering with an HTTP builder that injects context.
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// BAD — handler ctx has no type information
|
|
325
|
+
const { Create } = Procedures()
|
|
326
|
+
|
|
327
|
+
Create('GetUser', {}, async (ctx, params) => {
|
|
328
|
+
ctx.userId // Type error: Property 'userId' does not exist
|
|
329
|
+
})
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Fix:** Always specify the context type.
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// GOOD
|
|
336
|
+
const { Create } = Procedures<{ userId: string }>()
|
|
337
|
+
|
|
338
|
+
Create('GetUser', {}, async (ctx, params) => {
|
|
339
|
+
ctx.userId // Typed correctly
|
|
340
|
+
})
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## 12. Registering Standard Procedures with HonoStreamAppBuilder
|
|
346
|
+
|
|
347
|
+
**Problem:** Using `Create` procedures with `HonoStreamAppBuilder` — it only handles `CreateStream` procedures.
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
// BAD — Create procedures are silently ignored
|
|
351
|
+
const { Create, CreateStream } = Procedures<AppContext, RPCConfig>()
|
|
352
|
+
Create('GetUser', { scope: 'users', version: 1 }, handler)
|
|
353
|
+
CreateStream('StreamEvents', { scope: 'events', version: 1 }, streamHandler)
|
|
354
|
+
|
|
355
|
+
new HonoStreamAppBuilder()
|
|
356
|
+
.register(factory, ctx) // Only StreamEvents is registered
|
|
357
|
+
.build()
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Fix:** Use `HonoRPCAppBuilder` or `ExpressRPCAppBuilder` for standard procedures. Use `HonoStreamAppBuilder` only for stream procedures.
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// GOOD
|
|
364
|
+
const rpcApp = new HonoRPCAppBuilder().register(RPC, ctx).build()
|
|
365
|
+
const streamApp = new HonoStreamAppBuilder().register(StreamRPC, ctx).build()
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## 13. Missing Schema at Registration Time
|
|
371
|
+
|
|
372
|
+
**Problem:** Providing malformed or incompatible schema objects.
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
// BAD — plain objects are not valid schemas
|
|
376
|
+
Create('GetUser', {
|
|
377
|
+
schema: {
|
|
378
|
+
params: { type: 'object', properties: { id: { type: 'string' } } },
|
|
379
|
+
},
|
|
380
|
+
}, handler)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Fix:** Use TypeBox schema builders.
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// GOOD — TypeBox
|
|
387
|
+
Create('GetUser', {
|
|
388
|
+
schema: { params: Type.Object({ id: Type.String() }) },
|
|
389
|
+
}, handler)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**Why:** ts-procedures detects schema type via TypeBox's `~kind` symbol. Plain JSON Schema objects are not recognized and will throw `ProcedureRegistrationError`.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## 14. Not Handling Pre-Stream vs Mid-Stream Errors Differently
|
|
397
|
+
|
|
398
|
+
**Problem:** Using a single error handler for both validation errors (before streaming) and runtime errors (during streaming).
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// BAD — onError doesn't apply to streaming
|
|
402
|
+
new HonoStreamAppBuilder({
|
|
403
|
+
onError: (proc, c, err) => { /* This doesn't exist */ },
|
|
404
|
+
})
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Fix:** Use `onPreStreamError` for validation errors and `onMidStreamError` for runtime errors.
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
// GOOD
|
|
411
|
+
new HonoStreamAppBuilder({
|
|
412
|
+
onPreStreamError: (proc, c, error) => {
|
|
413
|
+
// Validation/context error — return normal HTTP response
|
|
414
|
+
return c.json({ error: error.message }, 400)
|
|
415
|
+
},
|
|
416
|
+
onMidStreamError: (proc, c, error) => {
|
|
417
|
+
// Runtime error during streaming — yield error event, then close
|
|
418
|
+
return { data: { error: error.message }, closeStream: true }
|
|
419
|
+
},
|
|
420
|
+
})
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## 15. Not Using extendProcedureDoc for Documentation
|
|
426
|
+
|
|
427
|
+
**Problem:** Manually building documentation from procedure metadata instead of using the built-in extension point.
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
// BAD — manual doc building, fragile and incomplete
|
|
431
|
+
const docs = getProcedures().map(p => ({
|
|
432
|
+
name: p.name,
|
|
433
|
+
path: `/${p.config.scope}/${p.name}/${p.config.version}`,
|
|
434
|
+
}))
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**Fix:** Use `extendProcedureDoc` in the HTTP builder registration.
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
// GOOD — base doc auto-generated with correct paths and schemas
|
|
441
|
+
builder.register(factory, context, ({ base, procedure }) => ({
|
|
442
|
+
summary: procedure.config.description,
|
|
443
|
+
tags: [base.scope],
|
|
444
|
+
}))
|
|
445
|
+
|
|
446
|
+
// Access via builder.docs
|
|
447
|
+
console.log(builder.docs)
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Why:** The builder generates correct kebab-case paths, includes JSON schemas for body/response, and merges your extensions. Manual building duplicates this logic and easily drifts.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## 16. Using Async Context Factory Without Error Handling
|
|
455
|
+
|
|
456
|
+
**Problem:** Async context factories that throw unhandled errors, crashing the request.
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
// BAD — if authenticate() throws, request crashes
|
|
460
|
+
builder.register(factory, async (req) => {
|
|
461
|
+
const user = await authenticate(req.headers.authorization)
|
|
462
|
+
return { userId: user.id }
|
|
463
|
+
})
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Fix:** Use the builder's `onError` callback to handle context resolution failures gracefully.
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
// GOOD
|
|
470
|
+
new ExpressRPCAppBuilder({
|
|
471
|
+
onError: (procedure, req, res, error) => {
|
|
472
|
+
if (error.message.includes('Unauthorized')) {
|
|
473
|
+
res.status(401).json({ error: 'Unauthorized' })
|
|
474
|
+
} else {
|
|
475
|
+
res.status(500).json({ error: 'Internal server error' })
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
})
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## 17. Using Both schema.params and schema.input
|
|
484
|
+
|
|
485
|
+
**Problem:** Defining both `schema.params` and `schema.input` on the same procedure.
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
// BAD — throws ProcedureRegistrationError at registration
|
|
489
|
+
Create('GetUser', {
|
|
490
|
+
path: '/users/:id',
|
|
491
|
+
method: 'get',
|
|
492
|
+
schema: {
|
|
493
|
+
params: Type.Object({ id: Type.String() }),
|
|
494
|
+
input: {
|
|
495
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
}, handler)
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**Fix:** Choose one. Use `schema.params` for flat RPC-style input, `schema.input` for structured multi-channel input.
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
// GOOD — schema.input for REST-style with per-channel validation
|
|
505
|
+
Create('GetUser', {
|
|
506
|
+
path: '/users/:id',
|
|
507
|
+
method: 'get',
|
|
508
|
+
schema: {
|
|
509
|
+
input: {
|
|
510
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
}, async (ctx, { pathParams }) => fetchUser(pathParams.id))
|
|
514
|
+
|
|
515
|
+
// GOOD — schema.params for simple RPC-style
|
|
516
|
+
Create('GetUser', {
|
|
517
|
+
scope: 'users', version: 1,
|
|
518
|
+
schema: { params: Type.Object({ id: Type.String() }) },
|
|
519
|
+
}, async (ctx, params) => fetchUser(params.id))
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**Why:** `schema.params` and `schema.input` are mutually exclusive by design. They represent different input paradigms — flat (RPC) vs structured (HTTP API).
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## 18. Mismatched Path Param Names in schema.input
|
|
527
|
+
|
|
528
|
+
**Problem:** Path template param names don't match `schema.input.pathParams` property names.
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
// BAD — path has :id but schema declares userId — throws at build time
|
|
532
|
+
Create('GetUser', {
|
|
533
|
+
path: '/users/:id',
|
|
534
|
+
method: 'get',
|
|
535
|
+
schema: {
|
|
536
|
+
input: {
|
|
537
|
+
pathParams: Type.Object({ userId: Type.String() }), // Wrong name!
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
}, handler)
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**Fix:** Ensure path param names match schema property names exactly.
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
// GOOD
|
|
547
|
+
Create('GetUser', {
|
|
548
|
+
path: '/users/:id',
|
|
549
|
+
method: 'get',
|
|
550
|
+
schema: {
|
|
551
|
+
input: {
|
|
552
|
+
pathParams: Type.Object({ id: Type.String() }), // Matches :id
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
}, handler)
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**Why:** `HonoAPIAppBuilder` validates at build time that `:param` names in the path match schema properties. A mismatch would cause runtime validation failures with confusing error messages.
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## 19. Forgetting build() is Async on HonoAPIAppBuilder
|
|
563
|
+
|
|
564
|
+
**Problem:** Not awaiting `build()` on `HonoAPIAppBuilder`.
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
// BAD — build() returns Promise<Hono>, not Hono
|
|
568
|
+
const app = new HonoAPIAppBuilder()
|
|
569
|
+
.register(API, ctx)
|
|
570
|
+
.build() // This is a Promise!
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**Fix:** Await the build call.
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
// GOOD
|
|
577
|
+
const app = await new HonoAPIAppBuilder()
|
|
578
|
+
.register(API, ctx)
|
|
579
|
+
.build()
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
**Why:** `HonoAPIAppBuilder.build()` is async because it resolves the query parser (lazy-loads `qs` optional peer dependency) once at build time for synchronous per-request usage.
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## Summary Table
|
|
587
|
+
|
|
588
|
+
| # | Anti-Pattern | Risk | Severity |
|
|
589
|
+
|---|-------------|------|----------|
|
|
590
|
+
| 1 | Raw Error instead of ctx.error() | Lost metadata, inconsistent handling | CRITICAL |
|
|
591
|
+
| 2 | Manual validation in handler | Duplicated logic, less thorough | WARNING |
|
|
592
|
+
| 3 | Expecting returnType validation | Silent data contract violations | CRITICAL |
|
|
593
|
+
| 4 | Ignoring ctx.signal | Resource waste on cancelled requests | WARNING |
|
|
594
|
+
| 5 | No signal check in stream loops | Infinite resource consumption | CRITICAL |
|
|
595
|
+
| 6 | Duplicate procedure names | ProcedureRegistrationError at startup | CRITICAL |
|
|
596
|
+
| 7 | validateYields without yieldType | Silent no-op, false confidence | WARNING |
|
|
597
|
+
| 8 | Swallowing errors | Hidden failures, debugging difficulty | CRITICAL |
|
|
598
|
+
| 9 | Manual type coercion | Unnecessary code, coercion mismatch | SUGGESTION |
|
|
599
|
+
| 10 | Expecting extra fields to survive | Silent data loss | WARNING |
|
|
600
|
+
| 11 | Missing context type | No type safety in handlers | WARNING |
|
|
601
|
+
| 12 | Create with HonoStreamAppBuilder | Procedures silently ignored | CRITICAL |
|
|
602
|
+
| 13 | Plain JSON Schema objects instead of TypeBox | ProcedureRegistrationError | CRITICAL |
|
|
603
|
+
| 14 | Wrong error handler for streams | Unhandled errors or wrong response format | WARNING |
|
|
604
|
+
| 15 | Manual doc building | Fragile, incomplete documentation | SUGGESTION |
|
|
605
|
+
| 16 | Unhandled async context factory | Request crashes | WARNING |
|
|
606
|
+
| 17 | Both schema.params and schema.input | ProcedureRegistrationError at startup | CRITICAL |
|
|
607
|
+
| 18 | Mismatched path param names | Build-time error or confusing validation failures | CRITICAL |
|
|
608
|
+
| 19 | Not awaiting HonoAPIAppBuilder.build() | Using unresolved Promise as app | CRITICAL |
|