@mind-fold/open-flow 0.1.17 → 0.2.2
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/dist/configurators/templates.d.ts.map +1 -1
- package/dist/configurators/templates.js +17 -6
- package/dist/configurators/templates.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +82 -6
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/templates/commands/break-loop.txt +107 -0
- package/dist/templates/commands/check-cross-layer.txt +153 -0
- package/dist/templates/commands/finish-work.txt +129 -0
- package/dist/templates/commands/index.d.ts +9 -5
- package/dist/templates/commands/index.d.ts.map +1 -1
- package/dist/templates/commands/index.js +16 -5
- package/dist/templates/commands/index.js.map +1 -1
- package/dist/templates/commands/init-agent.txt +100 -9
- package/dist/templates/commands/sync-from-runtime.txt +140 -0
- package/dist/templates/markdown/flow.md.txt +96 -84
- package/dist/templates/markdown/index.d.ts +21 -4
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +27 -4
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/structure/backend/database-guidelines.md.txt +247 -0
- package/dist/templates/markdown/structure/backend/directory-structure.md.txt +153 -0
- package/dist/templates/markdown/structure/backend/error-handling.md.txt +257 -0
- package/dist/templates/markdown/structure/backend/index.md.txt +88 -0
- package/dist/templates/markdown/structure/backend/logging-guidelines.md.txt +212 -0
- package/dist/templates/markdown/structure/backend/quality-guidelines.md.txt +219 -0
- package/dist/templates/markdown/structure/backend/type-safety.md.txt +192 -0
- package/dist/templates/markdown/structure/flows/code-reuse-thinking-guide.md.txt +343 -0
- package/dist/templates/markdown/structure/flows/cross-layer-thinking-guide.md.txt +283 -0
- package/dist/templates/markdown/structure/flows/index.md.txt +133 -0
- package/dist/templates/markdown/structure/flows/pre-implementation-checklist.md.txt +182 -0
- package/dist/templates/markdown/structure/flows/spec-flow-template.md.txt +145 -0
- package/dist/templates/markdown/structure/frontend/component-guidelines.md.txt +335 -0
- package/dist/templates/markdown/structure/frontend/directory-structure.md.txt +172 -0
- package/dist/templates/markdown/structure/frontend/hook-guidelines.md.txt +287 -0
- package/dist/templates/markdown/structure/frontend/index.md.txt +91 -0
- package/dist/templates/markdown/structure/frontend/quality-guidelines.md.txt +274 -0
- package/dist/templates/markdown/structure/frontend/state-management.md.txt +293 -0
- package/dist/templates/markdown/structure/frontend/type-safety.md.txt +275 -0
- package/package.json +2 -2
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Database Guidelines
|
|
2
|
+
|
|
3
|
+
> ORM patterns, batch operations, and query optimization
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Core Rules
|
|
8
|
+
|
|
9
|
+
1. **No await in loops** - Use batch operations
|
|
10
|
+
2. **Use transactions** - For multi-table operations
|
|
11
|
+
3. **Define relations** - Enable relational queries
|
|
12
|
+
4. **Handle timestamps** - Consistent format across layers
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## No Await in Loops (N+1 Problem)
|
|
17
|
+
|
|
18
|
+
The N+1 query problem is one of the most common performance issues.
|
|
19
|
+
|
|
20
|
+
### Problem
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// ❌ BAD: N+1 queries (1 + N database calls)
|
|
24
|
+
const users = await db.select().from(users).all();
|
|
25
|
+
for (const user of users) {
|
|
26
|
+
const posts = await db.select().from(posts).where(eq(posts.userId, user.id)).all();
|
|
27
|
+
user.posts = posts;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Solution 1: Relational Queries
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// ✅ GOOD: Single query with JOIN
|
|
35
|
+
const usersWithPosts = await db.query.users.findMany({
|
|
36
|
+
with: {
|
|
37
|
+
posts: true,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Solution 2: Batch Lookup with inArray
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// ✅ GOOD: Two queries total (1 for users, 1 for all posts)
|
|
46
|
+
import { inArray } from 'drizzle-orm';
|
|
47
|
+
|
|
48
|
+
const users = await db.select().from(users).all();
|
|
49
|
+
const userIds = users.map(u => u.id);
|
|
50
|
+
|
|
51
|
+
const allPosts = await db
|
|
52
|
+
.select()
|
|
53
|
+
.from(posts)
|
|
54
|
+
.where(inArray(posts.userId, userIds))
|
|
55
|
+
.all();
|
|
56
|
+
|
|
57
|
+
// Group in memory
|
|
58
|
+
const postsByUser = groupBy(allPosts, 'userId');
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Solution 3: Promise.all (Use Carefully)
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// ⚠️ OK for small N, but watch connection limits
|
|
65
|
+
const results = await Promise.all(
|
|
66
|
+
ids.slice(0, 10).map(id =>
|
|
67
|
+
db.query.users.findFirst({ where: eq(users.id, id) })
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Batch Operations
|
|
75
|
+
|
|
76
|
+
### Bulk Insert
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Insert multiple rows in one statement
|
|
80
|
+
await db.insert(users).values([
|
|
81
|
+
{ id: '1', email: 'a@example.com', name: 'Alice' },
|
|
82
|
+
{ id: '2', email: 'b@example.com', name: 'Bob' },
|
|
83
|
+
]).run();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Upsert (Insert or Update)
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
await db.insert(users)
|
|
90
|
+
.values(userData)
|
|
91
|
+
.onConflictDoUpdate({
|
|
92
|
+
target: users.id,
|
|
93
|
+
set: {
|
|
94
|
+
name: userData.name,
|
|
95
|
+
updatedAt: new Date(),
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
.run();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Bulk Update with inArray
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Update multiple rows matching condition
|
|
105
|
+
await db.update(users)
|
|
106
|
+
.set({ status: 'archived' })
|
|
107
|
+
.where(inArray(users.id, idsToArchive))
|
|
108
|
+
.run();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Transactions
|
|
114
|
+
|
|
115
|
+
Use transactions for operations that must be atomic.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// All-or-nothing: either all succeed or all rollback
|
|
119
|
+
const result = db.transaction((tx) => {
|
|
120
|
+
// Create user
|
|
121
|
+
const user = tx.insert(users).values({
|
|
122
|
+
id: userId,
|
|
123
|
+
email: input.email,
|
|
124
|
+
}).returning().get();
|
|
125
|
+
|
|
126
|
+
// Create default settings
|
|
127
|
+
tx.insert(userSettings).values({
|
|
128
|
+
userId: user.id,
|
|
129
|
+
theme: 'light',
|
|
130
|
+
}).run();
|
|
131
|
+
|
|
132
|
+
// Create audit log
|
|
133
|
+
tx.insert(auditLogs).values({
|
|
134
|
+
action: 'user_created',
|
|
135
|
+
entityId: user.id,
|
|
136
|
+
}).run();
|
|
137
|
+
|
|
138
|
+
return user;
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Timestamp Handling
|
|
145
|
+
|
|
146
|
+
### Use Milliseconds
|
|
147
|
+
|
|
148
|
+
JavaScript `Date.now()` returns milliseconds. Store timestamps consistently.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// Schema definition
|
|
152
|
+
export const users = sqliteTable('users', {
|
|
153
|
+
// ...
|
|
154
|
+
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
|
155
|
+
.notNull()
|
|
156
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
157
|
+
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
|
158
|
+
.notNull()
|
|
159
|
+
.default(sql`(unixepoch() * 1000)`)
|
|
160
|
+
.$onUpdate(() => new Date()),
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Common Pitfall
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// ❌ BAD: Mixing seconds and milliseconds
|
|
168
|
+
const timestamp = Math.floor(Date.now() / 1000); // Seconds
|
|
169
|
+
new Date(timestamp); // Wrong! Expects milliseconds
|
|
170
|
+
|
|
171
|
+
// ✅ GOOD: Consistent milliseconds
|
|
172
|
+
const timestamp = Date.now(); // Milliseconds
|
|
173
|
+
new Date(timestamp); // Correct
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Query Patterns
|
|
179
|
+
|
|
180
|
+
### Pagination
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
async function listUsers(page: number, pageSize: number) {
|
|
184
|
+
const offset = (page - 1) * pageSize;
|
|
185
|
+
|
|
186
|
+
const [users, countResult] = await Promise.all([
|
|
187
|
+
db.select()
|
|
188
|
+
.from(users)
|
|
189
|
+
.limit(pageSize)
|
|
190
|
+
.offset(offset)
|
|
191
|
+
.all(),
|
|
192
|
+
db.select({ count: count() })
|
|
193
|
+
.from(users)
|
|
194
|
+
.get(),
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
data: users,
|
|
199
|
+
total: countResult?.count ?? 0,
|
|
200
|
+
page,
|
|
201
|
+
pageSize,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Soft Delete
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// Schema with soft delete
|
|
210
|
+
export const users = sqliteTable('users', {
|
|
211
|
+
// ...
|
|
212
|
+
isDeleted: integer('is_deleted', { mode: 'boolean' }).notNull().default(false),
|
|
213
|
+
deletedAt: integer('deleted_at', { mode: 'timestamp_ms' }),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Query excludes deleted by default
|
|
217
|
+
const activeUsers = await db
|
|
218
|
+
.select()
|
|
219
|
+
.from(users)
|
|
220
|
+
.where(eq(users.isDeleted, false))
|
|
221
|
+
.all();
|
|
222
|
+
|
|
223
|
+
// Soft delete
|
|
224
|
+
await db.update(users)
|
|
225
|
+
.set({
|
|
226
|
+
isDeleted: true,
|
|
227
|
+
deletedAt: new Date()
|
|
228
|
+
})
|
|
229
|
+
.where(eq(users.id, userId))
|
|
230
|
+
.run();
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Summary
|
|
236
|
+
|
|
237
|
+
| Rule | Description |
|
|
238
|
+
|------|-------------|
|
|
239
|
+
| No await in loops | Use `with:`, `inArray`, or batch queries |
|
|
240
|
+
| Use transactions | For atomic multi-table operations |
|
|
241
|
+
| Millisecond timestamps | Match JavaScript Date behavior |
|
|
242
|
+
| Soft delete | Use `isDeleted` flag, not hard delete |
|
|
243
|
+
| Pagination | Always limit results for list endpoints |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
**Language**: All documentation must be written in **English**.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Directory Structure
|
|
2
|
+
|
|
3
|
+
> Module organization and file layout for backend development
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Standard Layout
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
server/
|
|
11
|
+
├── db/ # Database layer
|
|
12
|
+
│ ├── client.ts # Database client initialization
|
|
13
|
+
│ ├── schema.ts # Table definitions
|
|
14
|
+
│ └── migrations/ # Migration files
|
|
15
|
+
│
|
|
16
|
+
├── lib/ # Shared utilities
|
|
17
|
+
│ ├── logger.ts # Logger setup
|
|
18
|
+
│ ├── errors.ts # Custom error classes
|
|
19
|
+
│ └── utils.ts # Helper functions
|
|
20
|
+
│
|
|
21
|
+
├── middleware/ # Middleware
|
|
22
|
+
│ ├── auth.ts # Authentication
|
|
23
|
+
│ ├── error-handler.ts # Error handling
|
|
24
|
+
│ └── request-context.ts # Request context
|
|
25
|
+
│
|
|
26
|
+
├── routes/ # API routes (domain-driven)
|
|
27
|
+
│ └── {domain}/
|
|
28
|
+
│ ├── types.ts # Zod schemas + TypeScript types
|
|
29
|
+
│ ├── router.ts # Route definitions
|
|
30
|
+
│ ├── procedures/ # One file per endpoint
|
|
31
|
+
│ │ ├── list.ts
|
|
32
|
+
│ │ ├── get.ts
|
|
33
|
+
│ │ ├── create.ts
|
|
34
|
+
│ │ ├── update.ts
|
|
35
|
+
│ │ └── delete.ts
|
|
36
|
+
│ └── lib/ # Domain-specific logic
|
|
37
|
+
│
|
|
38
|
+
└── types.ts # Shared types (AppContext, etc.)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Module Structure
|
|
44
|
+
|
|
45
|
+
Each domain module follows this pattern:
|
|
46
|
+
|
|
47
|
+
### types.ts - Schema Definitions
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { z } from 'zod';
|
|
51
|
+
|
|
52
|
+
// Input schemas
|
|
53
|
+
export const createUserInput = z.object({
|
|
54
|
+
email: z.string().email(),
|
|
55
|
+
name: z.string().min(1),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const updateUserInput = z.object({
|
|
59
|
+
name: z.string().min(1).optional(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Output schemas
|
|
63
|
+
export const userOutput = z.object({
|
|
64
|
+
id: z.string(),
|
|
65
|
+
email: z.string(),
|
|
66
|
+
name: z.string(),
|
|
67
|
+
createdAt: z.date(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Infer types from schemas
|
|
71
|
+
export type CreateUserInput = z.infer<typeof createUserInput>;
|
|
72
|
+
export type UpdateUserInput = z.infer<typeof updateUserInput>;
|
|
73
|
+
export type UserOutput = z.infer<typeof userOutput>;
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### router.ts - Route Definitions
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { Hono } from 'hono';
|
|
80
|
+
import { zValidator } from '@hono/zod-validator';
|
|
81
|
+
import { createUserInput, updateUserInput } from './types';
|
|
82
|
+
import { createUser } from './procedures/create';
|
|
83
|
+
import { getUser } from './procedures/get';
|
|
84
|
+
import { listUsers } from './procedures/list';
|
|
85
|
+
|
|
86
|
+
const app = new Hono();
|
|
87
|
+
|
|
88
|
+
app.get('/', listUsers);
|
|
89
|
+
app.get('/:id', getUser);
|
|
90
|
+
app.post('/', zValidator('json', createUserInput), createUser);
|
|
91
|
+
app.patch('/:id', zValidator('json', updateUserInput), updateUser);
|
|
92
|
+
|
|
93
|
+
export default app;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### procedures/*.ts - Business Logic
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// procedures/create.ts
|
|
100
|
+
import type { Context } from 'hono';
|
|
101
|
+
import { db } from '@/db/client';
|
|
102
|
+
import { users } from '@/db/schema';
|
|
103
|
+
import type { CreateUserInput } from '../types';
|
|
104
|
+
|
|
105
|
+
export async function createUser(c: Context) {
|
|
106
|
+
const input = c.req.valid('json') as CreateUserInput;
|
|
107
|
+
const logger = c.get('logger');
|
|
108
|
+
|
|
109
|
+
logger.info('creating_user', { email: input.email });
|
|
110
|
+
|
|
111
|
+
const user = await db.insert(users).values({
|
|
112
|
+
id: generateId(),
|
|
113
|
+
email: input.email,
|
|
114
|
+
name: input.name,
|
|
115
|
+
}).returning().get();
|
|
116
|
+
|
|
117
|
+
return c.json(user);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Naming Conventions
|
|
124
|
+
|
|
125
|
+
| Type | Convention | Example |
|
|
126
|
+
|------|------------|---------|
|
|
127
|
+
| Files | kebab-case | `user-service.ts` |
|
|
128
|
+
| Directories | kebab-case | `user-management/` |
|
|
129
|
+
| Types | PascalCase | `UserOutput` |
|
|
130
|
+
| Functions | camelCase | `createUser` |
|
|
131
|
+
| Constants | SCREAMING_SNAKE | `MAX_RETRY_COUNT` |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Best Practices
|
|
136
|
+
|
|
137
|
+
### DO
|
|
138
|
+
|
|
139
|
+
- Keep procedures focused on single responsibility
|
|
140
|
+
- Put shared logic in `lib/` directory
|
|
141
|
+
- Use descriptive file names (`create-with-team.ts` not `create2.ts`)
|
|
142
|
+
- Group related functionality in domain modules
|
|
143
|
+
|
|
144
|
+
### DON'T
|
|
145
|
+
|
|
146
|
+
- Mix multiple domains in one module
|
|
147
|
+
- Put business logic in router files
|
|
148
|
+
- Create deeply nested directory structures
|
|
149
|
+
- Use generic names like `utils.ts` in domain modules
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
**Language**: All documentation must be written in **English**.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Error Handling
|
|
2
|
+
|
|
3
|
+
> Error types, handling strategies, and user-facing messages
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Error Types
|
|
8
|
+
|
|
9
|
+
### Application Errors
|
|
10
|
+
|
|
11
|
+
Define custom error classes for different error categories:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// lib/errors.ts
|
|
15
|
+
export class AppError extends Error {
|
|
16
|
+
constructor(
|
|
17
|
+
message: string,
|
|
18
|
+
public code: string,
|
|
19
|
+
public statusCode: number = 500,
|
|
20
|
+
public details?: Record<string, unknown>
|
|
21
|
+
) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'AppError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class NotFoundError extends AppError {
|
|
28
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
29
|
+
super(message, 'NOT_FOUND', 404, details);
|
|
30
|
+
this.name = 'NotFoundError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class ValidationError extends AppError {
|
|
35
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
36
|
+
super(message, 'VALIDATION_ERROR', 400, details);
|
|
37
|
+
this.name = 'ValidationError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class UnauthorizedError extends AppError {
|
|
42
|
+
constructor(message: string = 'Unauthorized') {
|
|
43
|
+
super(message, 'UNAUTHORIZED', 401);
|
|
44
|
+
this.name = 'UnauthorizedError';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class ForbiddenError extends AppError {
|
|
49
|
+
constructor(message: string = 'Forbidden') {
|
|
50
|
+
super(message, 'FORBIDDEN', 403);
|
|
51
|
+
this.name = 'ForbiddenError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Error Handling Middleware
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// middleware/error-handler.ts
|
|
62
|
+
import type { Context, Next } from 'hono';
|
|
63
|
+
import { AppError } from '@/lib/errors';
|
|
64
|
+
|
|
65
|
+
export async function errorHandler(c: Context, next: Next) {
|
|
66
|
+
try {
|
|
67
|
+
await next();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const logger = c.get('logger');
|
|
70
|
+
|
|
71
|
+
if (error instanceof AppError) {
|
|
72
|
+
logger.warn('app_error', {
|
|
73
|
+
code: error.code,
|
|
74
|
+
message: error.message,
|
|
75
|
+
details: error.details,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return c.json({
|
|
79
|
+
error: {
|
|
80
|
+
code: error.code,
|
|
81
|
+
message: error.message,
|
|
82
|
+
details: error.details,
|
|
83
|
+
},
|
|
84
|
+
}, error.statusCode);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Unexpected errors
|
|
88
|
+
logger.error('unhandled_error', error);
|
|
89
|
+
|
|
90
|
+
return c.json({
|
|
91
|
+
error: {
|
|
92
|
+
code: 'INTERNAL_ERROR',
|
|
93
|
+
message: 'An unexpected error occurred',
|
|
94
|
+
},
|
|
95
|
+
}, 500);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Error Handling Patterns
|
|
103
|
+
|
|
104
|
+
### Route Handlers
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
app.get('/users/:id', async (c) => {
|
|
108
|
+
const userId = c.req.param('id');
|
|
109
|
+
|
|
110
|
+
const user = await db.query.users.findFirst({
|
|
111
|
+
where: eq(users.id, userId),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!user) {
|
|
115
|
+
throw new NotFoundError(`User ${userId} not found`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return c.json(user);
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Service Layer
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
async function transferFunds(fromId: string, toId: string, amount: number) {
|
|
126
|
+
const fromAccount = await getAccount(fromId);
|
|
127
|
+
if (!fromAccount) {
|
|
128
|
+
throw new NotFoundError('Source account not found');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (fromAccount.balance < amount) {
|
|
132
|
+
throw new ValidationError('Insufficient funds', {
|
|
133
|
+
available: fromAccount.balance,
|
|
134
|
+
requested: amount,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Proceed with transfer...
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### External API Calls
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
async function fetchExternalData(id: string) {
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch(`https://api.example.com/data/${id}`);
|
|
148
|
+
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
if (response.status === 404) {
|
|
151
|
+
throw new NotFoundError('External resource not found');
|
|
152
|
+
}
|
|
153
|
+
throw new AppError(
|
|
154
|
+
'External API error',
|
|
155
|
+
'EXTERNAL_API_ERROR',
|
|
156
|
+
502,
|
|
157
|
+
{ status: response.status }
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return await response.json();
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error instanceof AppError) {
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
throw new AppError(
|
|
167
|
+
'Failed to fetch external data',
|
|
168
|
+
'EXTERNAL_API_ERROR',
|
|
169
|
+
502,
|
|
170
|
+
{ originalError: String(error) }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Validation Errors
|
|
179
|
+
|
|
180
|
+
### Zod Integration
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { z, ZodError } from 'zod';
|
|
184
|
+
|
|
185
|
+
app.post('/users', async (c) => {
|
|
186
|
+
try {
|
|
187
|
+
const input = createUserSchema.parse(await c.req.json());
|
|
188
|
+
// Process valid input...
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error instanceof ZodError) {
|
|
191
|
+
throw new ValidationError('Invalid input', {
|
|
192
|
+
errors: error.errors.map(e => ({
|
|
193
|
+
path: e.path.join('.'),
|
|
194
|
+
message: e.message,
|
|
195
|
+
})),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Response Format
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"error": {
|
|
208
|
+
"code": "VALIDATION_ERROR",
|
|
209
|
+
"message": "Invalid input",
|
|
210
|
+
"details": {
|
|
211
|
+
"errors": [
|
|
212
|
+
{ "path": "email", "message": "Invalid email format" },
|
|
213
|
+
{ "path": "age", "message": "Must be a positive number" }
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Error Codes
|
|
223
|
+
|
|
224
|
+
Maintain a consistent set of error codes:
|
|
225
|
+
|
|
226
|
+
| Code | HTTP Status | Description |
|
|
227
|
+
|------|-------------|-------------|
|
|
228
|
+
| `NOT_FOUND` | 404 | Resource not found |
|
|
229
|
+
| `VALIDATION_ERROR` | 400 | Invalid input |
|
|
230
|
+
| `UNAUTHORIZED` | 401 | Authentication required |
|
|
231
|
+
| `FORBIDDEN` | 403 | Permission denied |
|
|
232
|
+
| `CONFLICT` | 409 | Resource conflict (duplicate) |
|
|
233
|
+
| `RATE_LIMITED` | 429 | Too many requests |
|
|
234
|
+
| `INTERNAL_ERROR` | 500 | Unexpected server error |
|
|
235
|
+
| `EXTERNAL_API_ERROR` | 502 | External service failure |
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Best Practices
|
|
240
|
+
|
|
241
|
+
### DO
|
|
242
|
+
|
|
243
|
+
- Use specific error types for different scenarios
|
|
244
|
+
- Include helpful details for debugging
|
|
245
|
+
- Log errors with context
|
|
246
|
+
- Return consistent error response format
|
|
247
|
+
|
|
248
|
+
### DON'T
|
|
249
|
+
|
|
250
|
+
- Expose internal error details to clients
|
|
251
|
+
- Use generic error messages
|
|
252
|
+
- Swallow errors silently
|
|
253
|
+
- Return stack traces in production
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
**Language**: All documentation must be written in **English**.
|