ts-procedures 5.2.0 → 5.3.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 +60 -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 +173 -0
- package/agent_config/claude-code/skills/guide/SKILL.md +142 -0
- package/agent_config/claude-code/skills/guide/anti-patterns.md +502 -0
- package/agent_config/claude-code/skills/guide/api-reference.md +550 -0
- package/agent_config/claude-code/skills/guide/patterns.md +572 -0
- package/agent_config/claude-code/skills/review/SKILL.md +53 -0
- package/agent_config/claude-code/skills/review/checklist.md +141 -0
- package/agent_config/claude-code/skills/scaffold/SKILL.md +54 -0
- package/agent_config/claude-code/skills/scaffold/templates/express-rpc.md +134 -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 +255 -0
- package/agent_config/cursor/cursorrules +255 -0
- package/agent_config/lib/install-claude.mjs +109 -0
- package/package.json +7 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: guide
|
|
3
|
+
description: "ts-procedures framework reference — core API, schema validation, error classes, context shape, HTTP implementations, and decision framework. Auto-loaded when ts-procedures imports are detected."
|
|
4
|
+
invocable_by:
|
|
5
|
+
- model
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# ts-procedures Framework Reference
|
|
9
|
+
|
|
10
|
+
You are assisting a developer using **ts-procedures**, a TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition. Always follow these rules when writing or reviewing code.
|
|
11
|
+
|
|
12
|
+
## Core Flow
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Procedures<TContext, TExtendedConfig>(builder?)
|
|
16
|
+
↓
|
|
17
|
+
Create(name, config, handler) → Standard async procedure
|
|
18
|
+
CreateStream(name, config, handler) → Streaming async generator
|
|
19
|
+
↓
|
|
20
|
+
Returns: { [name]: handler, procedure: handler, info: metadata }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Factory API
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
const { Create, CreateStream, getProcedures, getProcedure, removeProcedure, clear } =
|
|
27
|
+
Procedures<TContext, TExtendedConfig>({
|
|
28
|
+
onCreate: (procedure) => { /* called on each registration */ }
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Method | Purpose |
|
|
33
|
+
|--------|---------|
|
|
34
|
+
| `Create(name, config, handler)` | Register standard async procedure |
|
|
35
|
+
| `CreateStream(name, config, handler)` | Register streaming procedure (async generator) |
|
|
36
|
+
| `getProcedures()` | Returns array of all registered procedures |
|
|
37
|
+
| `getProcedure(name)` | Get single procedure by name |
|
|
38
|
+
| `removeProcedure(name)` | Remove procedure by name |
|
|
39
|
+
| `clear()` | Remove all procedures |
|
|
40
|
+
|
|
41
|
+
## Context
|
|
42
|
+
|
|
43
|
+
Handlers receive `(ctx, params)` where ctx includes:
|
|
44
|
+
|
|
45
|
+
| Property | Type | Availability |
|
|
46
|
+
|----------|------|-------------|
|
|
47
|
+
| Base context fields | `TContext` | Always |
|
|
48
|
+
| `error(message, meta?)` | `(string, object?) => ProcedureError` | Always |
|
|
49
|
+
| `signal` | `AbortSignal` | **Guaranteed** in CreateStream; optional in Create (present when HTTP impl provides it) |
|
|
50
|
+
| `isPrevalidated` | `boolean` | Set by HTTP impls to skip redundant validation |
|
|
51
|
+
|
|
52
|
+
## Schema System
|
|
53
|
+
|
|
54
|
+
Uses **TypeBox** for schema definitions (`import { Type } from 'typebox'`):
|
|
55
|
+
|
|
56
|
+
| Library | Detection | Example |
|
|
57
|
+
|---------|-----------|---------|
|
|
58
|
+
| **TypeBox** | `~kind` symbol | `Type.Object({ name: Type.String() })` |
|
|
59
|
+
|
|
60
|
+
### Validation Rules
|
|
61
|
+
|
|
62
|
+
- `schema.params` — **Validated at runtime** via AJV
|
|
63
|
+
- `schema.returnType` — **Documentation only**, never validated
|
|
64
|
+
- `schema.yieldType` — Validated only if `validateYields: true` in CreateStream config
|
|
65
|
+
- AJV config: `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`
|
|
66
|
+
|
|
67
|
+
## Error Classes
|
|
68
|
+
|
|
69
|
+
| Error Class | Trigger | Key Properties |
|
|
70
|
+
|-------------|---------|----------------|
|
|
71
|
+
| `ProcedureError` | `ctx.error()` in handlers, or unhandled handler exceptions | `procedureName`, `message`, `meta`, `cause`, `definedAt` |
|
|
72
|
+
| `ProcedureValidationError` | Schema params validation failure | `procedureName`, `errors[]` (AJV errors) |
|
|
73
|
+
| `ProcedureYieldValidationError` | Stream yield validation failure | `procedureName`, `errors[]` (AJV errors) |
|
|
74
|
+
| `ProcedureRegistrationError` | Invalid schema at registration time | `procedureName`, `message` |
|
|
75
|
+
|
|
76
|
+
All errors include `definedAt` (file:line:column) and enhanced stack traces pointing to the procedure definition location.
|
|
77
|
+
|
|
78
|
+
## HTTP Implementations
|
|
79
|
+
|
|
80
|
+
| Builder | Import | Transport |
|
|
81
|
+
|---------|--------|-----------|
|
|
82
|
+
| `ExpressRPCAppBuilder` | `ts-procedures/express-rpc` | POST JSON |
|
|
83
|
+
| `HonoRPCAppBuilder` | `ts-procedures/hono-rpc` | POST JSON |
|
|
84
|
+
| `HonoStreamAppBuilder` | `ts-procedures/hono-stream` | SSE or newline-delimited JSON |
|
|
85
|
+
|
|
86
|
+
### Route Path Format
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
{pathPrefix}/{scope}/{kebab-case-name}/{version}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Example: `Create('GetUser', { scope: 'users', version: 1 }, ...)` → `POST /users/get-user/1`
|
|
93
|
+
|
|
94
|
+
### Builder Pattern
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const app = new ExpressRPCAppBuilder({ pathPrefix: '/api' })
|
|
98
|
+
.register(factory, (req) => ({ /* context */ }))
|
|
99
|
+
.build()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Lifecycle Hooks (HTTP builders)
|
|
103
|
+
|
|
104
|
+
| Hook | When |
|
|
105
|
+
|------|------|
|
|
106
|
+
| `onRequestStart` | Before context resolution |
|
|
107
|
+
| `onRequestEnd` | After response sent |
|
|
108
|
+
| `onSuccess` | After successful handler execution |
|
|
109
|
+
| `onError` | On handler error (return custom response) |
|
|
110
|
+
| `onStreamStart` | Before first yield (HonoStreamAppBuilder) |
|
|
111
|
+
| `onStreamEnd` | After stream closes (HonoStreamAppBuilder) |
|
|
112
|
+
| `onPreStreamError` | Validation/context error before streaming starts |
|
|
113
|
+
| `onMidStreamError` | Error during streaming (return data to yield as final event) |
|
|
114
|
+
|
|
115
|
+
## Decision Framework
|
|
116
|
+
|
|
117
|
+
**Which procedure type?**
|
|
118
|
+
- Request → single response → **Create**
|
|
119
|
+
- Request → multiple values over time → **CreateStream**
|
|
120
|
+
|
|
121
|
+
**Which schema library?**
|
|
122
|
+
- Use **TypeBox** (`import { Type } from 'typebox'`)
|
|
123
|
+
- TypeBox schemas are valid JSON Schema directly
|
|
124
|
+
- Use `Type.Optional(...)` for optional fields
|
|
125
|
+
|
|
126
|
+
**Which HTTP implementation?**
|
|
127
|
+
- Express app → **ExpressRPCAppBuilder**
|
|
128
|
+
- Hono app (standard RPC) → **HonoRPCAppBuilder**
|
|
129
|
+
- Hono app (streaming) → **HonoStreamAppBuilder**
|
|
130
|
+
- SSE with browser EventSource → `streamMode: 'sse'`
|
|
131
|
+
- Simple text streaming → `streamMode: 'text'`
|
|
132
|
+
|
|
133
|
+
**How to group procedures?**
|
|
134
|
+
- By domain: `UserProcedures`, `OrderProcedures`
|
|
135
|
+
- By access level: `PublicRPC`, `AuthenticatedRPC`
|
|
136
|
+
- Each group = one `Procedures()` factory call
|
|
137
|
+
|
|
138
|
+
## Supporting Files
|
|
139
|
+
|
|
140
|
+
For complete API details, see `api-reference.md` in this skill directory.
|
|
141
|
+
For prescribed patterns with code examples, see `patterns.md`.
|
|
142
|
+
For common mistakes to avoid, see `anti-patterns.md`.
|
|
@@ -0,0 +1,502 @@
|
|
|
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
|
+
## Summary Table
|
|
484
|
+
|
|
485
|
+
| # | Anti-Pattern | Risk | Severity |
|
|
486
|
+
|---|-------------|------|----------|
|
|
487
|
+
| 1 | Raw Error instead of ctx.error() | Lost metadata, inconsistent handling | CRITICAL |
|
|
488
|
+
| 2 | Manual validation in handler | Duplicated logic, less thorough | WARNING |
|
|
489
|
+
| 3 | Expecting returnType validation | Silent data contract violations | CRITICAL |
|
|
490
|
+
| 4 | Ignoring ctx.signal | Resource waste on cancelled requests | WARNING |
|
|
491
|
+
| 5 | No signal check in stream loops | Infinite resource consumption | CRITICAL |
|
|
492
|
+
| 6 | Duplicate procedure names | ProcedureRegistrationError at startup | CRITICAL |
|
|
493
|
+
| 7 | validateYields without yieldType | Silent no-op, false confidence | WARNING |
|
|
494
|
+
| 8 | Swallowing errors | Hidden failures, debugging difficulty | CRITICAL |
|
|
495
|
+
| 9 | Manual type coercion | Unnecessary code, coercion mismatch | SUGGESTION |
|
|
496
|
+
| 10 | Expecting extra fields to survive | Silent data loss | WARNING |
|
|
497
|
+
| 11 | Missing context type | No type safety in handlers | WARNING |
|
|
498
|
+
| 12 | Create with HonoStreamAppBuilder | Procedures silently ignored | CRITICAL |
|
|
499
|
+
| 13 | Plain JSON Schema objects instead of TypeBox | ProcedureRegistrationError | CRITICAL |
|
|
500
|
+
| 14 | Wrong error handler for streams | Unhandled errors or wrong response format | WARNING |
|
|
501
|
+
| 15 | Manual doc building | Fragile, incomplete documentation | SUGGESTION |
|
|
502
|
+
| 16 | Unhandled async context factory | Request crashes | WARNING |
|