@donkeylabs/server 2.0.37 → 2.1.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/docs/code-organization.md +424 -0
- package/docs/project-structure.md +37 -26
- package/docs/swift-adapter.md +293 -0
- package/docs/versioning.md +351 -0
- package/package.json +1 -1
- package/src/client/base.ts +18 -5
- package/src/core/events.ts +54 -7
- package/src/core/health.ts +165 -0
- package/src/core/index.ts +12 -0
- package/src/core/jobs.ts +11 -4
- package/src/core/subprocess-bootstrap.ts +3 -0
- package/src/core/workflow-executor.ts +1 -0
- package/src/core/workflow-state-machine.ts +6 -5
- package/src/core/workflows.ts +3 -2
- package/src/core.ts +9 -1
- package/src/generator/index.ts +4 -0
- package/src/harness.ts +17 -5
- package/src/index.ts +21 -0
- package/src/router.ts +22 -2
- package/src/server.ts +458 -117
- package/src/versioning.ts +154 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# Code Organization Guide
|
|
2
|
+
|
|
3
|
+
Rules for structuring Donkeylabs plugins and routes as they grow. Follow these rules exactly — they prevent the most common structural problems.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. File Size Rules
|
|
8
|
+
|
|
9
|
+
| Threshold | Action |
|
|
10
|
+
|-----------|--------|
|
|
11
|
+
| Any file > 200 lines | Split it |
|
|
12
|
+
| Plugin `index.ts` > 300 lines | Extract service, types, helpers |
|
|
13
|
+
| Route handler > 50 lines | Move business logic to plugin service |
|
|
14
|
+
| Single function > 40 lines | Break into smaller functions |
|
|
15
|
+
|
|
16
|
+
**The 300-line rule for `index.ts`**: A plugin's `index.ts` should contain only the plugin definition and wiring. If it's over 300 lines, you have business logic that belongs in a separate `service.ts`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 2. Plugin File Structure
|
|
21
|
+
|
|
22
|
+
### Small plugin (< 200 lines total) — single file is fine
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
plugins/notifications/
|
|
26
|
+
├── index.ts # Everything here
|
|
27
|
+
├── schema.ts # If using database
|
|
28
|
+
└── migrations/
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Medium plugin (200–500 lines) — extract the service
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
plugins/orders/
|
|
35
|
+
├── index.ts # Plugin definition + wiring only
|
|
36
|
+
├── service.ts # Service class with business logic
|
|
37
|
+
├── schema.ts
|
|
38
|
+
└── migrations/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Large plugin (500+ lines) — full split
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
plugins/auth/
|
|
45
|
+
├── index.ts # Plugin definition + wiring only (< 100 lines)
|
|
46
|
+
├── types.ts # Interfaces, type aliases, enums
|
|
47
|
+
├── service.ts # Service class with business logic
|
|
48
|
+
├── helpers.ts # Pure utility functions
|
|
49
|
+
├── constants.ts # Configuration constants, magic strings
|
|
50
|
+
├── schema.ts
|
|
51
|
+
└── migrations/
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### What goes where
|
|
55
|
+
|
|
56
|
+
| File | Contains | Does NOT contain |
|
|
57
|
+
|------|----------|-----------------|
|
|
58
|
+
| `index.ts` | `createPlugin.define()`, imports, wiring | Business logic, type definitions, helpers |
|
|
59
|
+
| `service.ts` | Service class, all business methods | Plugin definition, Zod schemas |
|
|
60
|
+
| `types.ts` | Interfaces, type aliases, enums | Implementation code |
|
|
61
|
+
| `helpers.ts` | Pure functions (no `ctx` dependency) | Stateful logic, database calls |
|
|
62
|
+
| `constants.ts` | String literals, config defaults, enums | Functions, classes |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 3. Service Class Pattern
|
|
67
|
+
|
|
68
|
+
For any plugin with more than 2-3 trivial methods, use a service class.
|
|
69
|
+
|
|
70
|
+
### BAD: Inline object literal with all logic in `index.ts`
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// plugins/orders/index.ts — 800 lines, untestable, unreadable
|
|
74
|
+
export const ordersPlugin = createPlugin.withSchema<DB>().define({
|
|
75
|
+
name: "orders",
|
|
76
|
+
service: async (ctx) => ({
|
|
77
|
+
async create(data: OrderInput) {
|
|
78
|
+
// 40 lines of validation...
|
|
79
|
+
// 30 lines of database calls...
|
|
80
|
+
// 20 lines of event emission...
|
|
81
|
+
},
|
|
82
|
+
async fulfill(orderId: string) {
|
|
83
|
+
// 60 more lines...
|
|
84
|
+
},
|
|
85
|
+
async refund(orderId: string, reason: string) {
|
|
86
|
+
// 50 more lines...
|
|
87
|
+
},
|
|
88
|
+
// ... 10 more methods
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### GOOD: Service class in separate file
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// plugins/orders/service.ts
|
|
97
|
+
import type { PluginContext } from "../../core";
|
|
98
|
+
import type { DB } from "./schema";
|
|
99
|
+
|
|
100
|
+
export class OrdersService {
|
|
101
|
+
constructor(private ctx: PluginContext<DB>) {}
|
|
102
|
+
|
|
103
|
+
async create(data: OrderInput) {
|
|
104
|
+
const validated = this.validateOrder(data);
|
|
105
|
+
const order = await this.ctx.db
|
|
106
|
+
.insertInto("orders")
|
|
107
|
+
.values(validated)
|
|
108
|
+
.returningAll()
|
|
109
|
+
.executeTakeFirstOrThrow();
|
|
110
|
+
|
|
111
|
+
await this.ctx.core.events.emit("order.created", { orderId: order.id });
|
|
112
|
+
return order;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async fulfill(orderId: string) {
|
|
116
|
+
// Clear, focused method
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async refund(orderId: string, reason: string) {
|
|
120
|
+
// Clear, focused method
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private validateOrder(data: OrderInput) {
|
|
124
|
+
// Private helper — testable via public methods
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// plugins/orders/index.ts — thin wiring, ~30 lines
|
|
131
|
+
import { createPlugin } from "../../core";
|
|
132
|
+
import type { DB } from "./schema";
|
|
133
|
+
import { OrdersService } from "./service";
|
|
134
|
+
|
|
135
|
+
export const ordersPlugin = createPlugin.withSchema<DB>().define({
|
|
136
|
+
name: "orders",
|
|
137
|
+
service: async (ctx) => {
|
|
138
|
+
const svc = new OrdersService(ctx);
|
|
139
|
+
return {
|
|
140
|
+
create: svc.create.bind(svc),
|
|
141
|
+
fulfill: svc.fulfill.bind(svc),
|
|
142
|
+
refund: svc.refund.bind(svc),
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Why classes?
|
|
149
|
+
|
|
150
|
+
- **Testable**: Instantiate with a mock `ctx`, test methods in isolation
|
|
151
|
+
- **Readable**: Each method is focused, private helpers stay private
|
|
152
|
+
- **Navigable**: IDE jump-to-definition works, not lost in a 500-line object literal
|
|
153
|
+
- **Maintainable**: Adding a method doesn't balloon a single file
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 4. Generated Types — NEVER Use `as any`
|
|
158
|
+
|
|
159
|
+
After running `donkeylabs generate`, the framework produces fully-typed declarations. Use them.
|
|
160
|
+
|
|
161
|
+
### Rule: zero `as any` casts for framework types
|
|
162
|
+
|
|
163
|
+
If you find yourself writing `as any`, you either:
|
|
164
|
+
1. Haven't run `donkeylabs generate` yet, or
|
|
165
|
+
2. Are importing the wrong type
|
|
166
|
+
|
|
167
|
+
### Available generated types
|
|
168
|
+
|
|
169
|
+
| Type | Source | Usage |
|
|
170
|
+
|------|--------|-------|
|
|
171
|
+
| `AppContext` | Inferred from your server instance | Route handlers, middleware |
|
|
172
|
+
| `PluginRegistry` | Generated from registered plugins | `ctx.plugins.<name>` is fully typed |
|
|
173
|
+
| `ServiceRegistry` | Generated from `defineService()` calls | `ctx.services.<name>` is fully typed |
|
|
174
|
+
| Route types | Generated from router schemas | Import input/output types |
|
|
175
|
+
|
|
176
|
+
### BAD: Casting to `any`
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// Route handler with no type safety
|
|
180
|
+
router.route("create").typed({
|
|
181
|
+
input: z.object({ email: z.string() }),
|
|
182
|
+
handle: async (input: any, ctx: any) => {
|
|
183
|
+
const users = (ctx as any).plugins.users;
|
|
184
|
+
const result = await users.create(input);
|
|
185
|
+
return result as any;
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### GOOD: Using inferred types
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// Types flow automatically — no casts needed
|
|
194
|
+
router.route("create").typed({
|
|
195
|
+
input: z.object({ email: z.string() }),
|
|
196
|
+
handle: async (input, ctx) => {
|
|
197
|
+
// ctx.plugins.users is fully typed after `donkeylabs generate`
|
|
198
|
+
return ctx.plugins.users.create(input);
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### BAD: Typing `ctx` manually
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// Don't hand-write context types
|
|
207
|
+
service: async (ctx: { db: any; core: any; deps: any }) => {
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### GOOD: Let the framework infer
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
// ctx is fully typed by createPlugin's generics
|
|
214
|
+
service: async (ctx) => {
|
|
215
|
+
ctx.db; // Typed as Kysely<DB> (from withSchema<DB>())
|
|
216
|
+
ctx.core; // Typed as CoreServices
|
|
217
|
+
ctx.deps; // Typed based on dependencies array
|
|
218
|
+
ctx.config; // Typed as your config interface (from withConfig<T>())
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Getting your app's context type
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// In your server setup file
|
|
226
|
+
import { AppServer } from "@donkeylabs/server";
|
|
227
|
+
|
|
228
|
+
const server = new AppServer({ ... });
|
|
229
|
+
|
|
230
|
+
// Export the context type for use across your app
|
|
231
|
+
export type AppContext = typeof server extends AppServer<infer C> ? C : never;
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 5. Route Type Imports — Don't Duplicate Zod Schemas
|
|
237
|
+
|
|
238
|
+
When route schemas are defined in a router, don't recreate them elsewhere. Import the inferred types instead.
|
|
239
|
+
|
|
240
|
+
### BAD: Duplicating schemas
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// routes/users.ts
|
|
244
|
+
export const usersRouter = createRouter("users")
|
|
245
|
+
.route("create").typed({
|
|
246
|
+
input: z.object({ email: z.string(), name: z.string() }),
|
|
247
|
+
handle: async (input, ctx) => ctx.plugins.users.create(input),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// plugins/users/index.ts — duplicated schema!
|
|
251
|
+
const CreateUserInput = z.object({ email: z.string(), name: z.string() });
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Now you have two sources of truth. When one changes, the other doesn't.
|
|
255
|
+
|
|
256
|
+
### GOOD: Single source of truth
|
|
257
|
+
|
|
258
|
+
Define schemas in one place (the router or a shared schemas file), then import the inferred types:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// routes/users/users.schemas.ts — single source of truth
|
|
262
|
+
import { z } from "zod";
|
|
263
|
+
|
|
264
|
+
export const createUserInput = z.object({
|
|
265
|
+
email: z.string().email(),
|
|
266
|
+
name: z.string().min(1),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
export type CreateUserInput = z.infer<typeof createUserInput>;
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
// routes/users/index.ts
|
|
274
|
+
import { createUserInput } from "./users.schemas";
|
|
275
|
+
|
|
276
|
+
export const usersRouter = createRouter("users")
|
|
277
|
+
.route("create").typed({
|
|
278
|
+
input: createUserInput,
|
|
279
|
+
handle: async (input, ctx) => ctx.plugins.users.create(input),
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
// plugins/users/service.ts — imports the TYPE, not the schema
|
|
285
|
+
import type { CreateUserInput } from "../../routes/users/users.schemas";
|
|
286
|
+
|
|
287
|
+
export class UsersService {
|
|
288
|
+
async create(data: CreateUserInput) {
|
|
289
|
+
// ...
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Where to define schemas
|
|
295
|
+
|
|
296
|
+
| Scenario | Location |
|
|
297
|
+
|----------|----------|
|
|
298
|
+
| Used by one route only | Inline in the router file |
|
|
299
|
+
| Used by route + plugin service | `routes/<name>/<name>.schemas.ts` |
|
|
300
|
+
| Used across multiple features | Shared `schemas/` directory |
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## 6. Anti-Pattern Gallery
|
|
305
|
+
|
|
306
|
+
### Anti-pattern: Monolithic plugin file
|
|
307
|
+
|
|
308
|
+
**Symptom**: A single `index.ts` with 1000+ lines containing types, helpers, constants, and all service methods.
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
// BAD: plugins/billing/index.ts — 1200 lines
|
|
312
|
+
interface Invoice { ... }
|
|
313
|
+
interface PaymentMethod { ... }
|
|
314
|
+
interface Subscription { ... }
|
|
315
|
+
const TAX_RATES = { ... };
|
|
316
|
+
const CURRENCY_FORMATS = { ... };
|
|
317
|
+
function calculateTax(...) { ... }
|
|
318
|
+
function formatCurrency(...) { ... }
|
|
319
|
+
export const billingPlugin = createPlugin.define({
|
|
320
|
+
name: "billing",
|
|
321
|
+
service: async (ctx) => ({
|
|
322
|
+
// 800 lines of methods...
|
|
323
|
+
}),
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Fix**: Split into `types.ts`, `constants.ts`, `helpers.ts`, `service.ts`, and a thin `index.ts`.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
### Anti-pattern: `as any` to silence type errors
|
|
332
|
+
|
|
333
|
+
**Symptom**: TypeScript errors "fixed" by casting instead of using correct types.
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
// BAD
|
|
337
|
+
const user = await (ctx as any).plugins.users.getById(id);
|
|
338
|
+
return { data: result } as any;
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Fix**: Run `donkeylabs generate` to update types. If types are still wrong, check that your plugin is registered in the server and the service return type is correct.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
### Anti-pattern: Business logic in route handlers
|
|
346
|
+
|
|
347
|
+
**Symptom**: Route handlers with 30+ lines of logic instead of delegating to plugin services.
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
// BAD
|
|
351
|
+
router.route("create").typed({
|
|
352
|
+
input: createOrderInput,
|
|
353
|
+
handle: async (input, ctx) => {
|
|
354
|
+
// 50 lines of validation, DB calls, event emission...
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Fix**: Move logic to plugin service. Route handler should be a one-liner:
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
// GOOD
|
|
363
|
+
handle: async (input, ctx) => ctx.plugins.orders.create(input),
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
### Anti-pattern: Duplicated Zod schemas
|
|
369
|
+
|
|
370
|
+
**Symptom**: The same shape defined in both the router and the plugin.
|
|
371
|
+
|
|
372
|
+
**Fix**: Define once in a `.schemas.ts` file, import the schema in the router and the inferred type in the service. See [Section 5](#5-route-type-imports--dont-duplicate-zod-schemas).
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
### Anti-pattern: No dependency injection
|
|
377
|
+
|
|
378
|
+
**Symptom**: Service functions that import and call other services directly instead of receiving them through `ctx`.
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
// BAD: Hard-coded dependency
|
|
382
|
+
import { db } from "../../database";
|
|
383
|
+
|
|
384
|
+
export function createOrder(data: OrderInput) {
|
|
385
|
+
return db.insertInto("orders").values(data).execute();
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**Fix**: Use the plugin context:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
// GOOD: Injected via ctx
|
|
393
|
+
export class OrdersService {
|
|
394
|
+
constructor(private ctx: PluginContext<DB>) {}
|
|
395
|
+
|
|
396
|
+
async createOrder(data: OrderInput) {
|
|
397
|
+
return this.ctx.db.insertInto("orders").values(data).execute();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Quick Reference
|
|
405
|
+
|
|
406
|
+
```
|
|
407
|
+
Is your plugin file > 200 lines?
|
|
408
|
+
├─ No → Single file is fine
|
|
409
|
+
└─ Yes → Is it > 500 lines?
|
|
410
|
+
├─ No → Extract service.ts
|
|
411
|
+
└─ Yes → Full split: types.ts, service.ts, helpers.ts, constants.ts
|
|
412
|
+
|
|
413
|
+
Is your route handler > 3 lines?
|
|
414
|
+
├─ No → Fine as-is
|
|
415
|
+
└─ Yes → Move logic to plugin service
|
|
416
|
+
|
|
417
|
+
Are you writing `as any`?
|
|
418
|
+
├─ No → Good
|
|
419
|
+
└─ Yes → Run `donkeylabs generate`, use inferred types
|
|
420
|
+
|
|
421
|
+
Are you copying a Zod schema to a second file?
|
|
422
|
+
├─ No → Good
|
|
423
|
+
└─ Yes → Create a shared .schemas.ts, import from there
|
|
424
|
+
```
|
|
@@ -303,40 +303,51 @@ service: (ctx) => ({
|
|
|
303
303
|
})
|
|
304
304
|
```
|
|
305
305
|
|
|
306
|
-
### DON'T: Create Unnecessary Files
|
|
306
|
+
### DON'T: Create Unnecessary Files (But DO Split When Needed)
|
|
307
307
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
308
|
+
The right structure depends on plugin size. Don't create files you don't need, but don't cram everything into one file either.
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
// Small plugin (< 200 lines) — single file is fine
|
|
312
|
+
plugins/notifications/
|
|
313
|
+
├── index.ts // Everything here
|
|
312
314
|
├── schema.ts
|
|
313
|
-
├── types/
|
|
314
|
-
│ ├── index.ts
|
|
315
|
-
│ ├── user.ts
|
|
316
|
-
│ ├── session.ts
|
|
317
|
-
│ └── token.ts
|
|
318
|
-
├── services/
|
|
319
|
-
│ ├── index.ts
|
|
320
|
-
│ ├── user.service.ts
|
|
321
|
-
│ └── auth.service.ts
|
|
322
|
-
├── handlers/
|
|
323
|
-
│ └── custom.handler.ts
|
|
324
|
-
├── middleware/
|
|
325
|
-
│ └── auth.middleware.ts
|
|
326
|
-
├── utils/
|
|
327
|
-
│ ├── hash.ts
|
|
328
|
-
│ └── token.ts
|
|
329
315
|
└── migrations/
|
|
330
|
-
```
|
|
331
316
|
|
|
332
|
-
|
|
333
|
-
|
|
317
|
+
// Medium plugin (200-500 lines) — extract service
|
|
318
|
+
plugins/orders/
|
|
319
|
+
├── index.ts // Plugin definition + wiring only
|
|
320
|
+
├── service.ts // Service class with business logic
|
|
321
|
+
├── schema.ts
|
|
322
|
+
└── migrations/
|
|
323
|
+
|
|
324
|
+
// Large plugin (500+ lines) — full split
|
|
334
325
|
plugins/auth/
|
|
335
|
-
├── index.ts
|
|
326
|
+
├── index.ts // Plugin definition + wiring (< 100 lines)
|
|
327
|
+
├── types.ts // Interfaces, type aliases
|
|
328
|
+
├── service.ts // Service class with business logic
|
|
329
|
+
├── helpers.ts // Pure utility functions
|
|
330
|
+
├── constants.ts // Configuration constants
|
|
336
331
|
├── schema.ts
|
|
337
332
|
└── migrations/
|
|
338
333
|
```
|
|
339
334
|
|
|
335
|
+
**Avoid** deeply nested subdirectories (`types/`, `services/`, `utils/`). Flat files at the plugin root are enough:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
// BAD: Nested subdirectories with barrel exports
|
|
339
|
+
plugins/auth/types/index.ts
|
|
340
|
+
plugins/auth/types/user.ts
|
|
341
|
+
plugins/auth/services/index.ts
|
|
342
|
+
plugins/auth/services/auth.service.ts
|
|
343
|
+
|
|
344
|
+
// GOOD: Flat files at plugin root
|
|
345
|
+
plugins/auth/types.ts
|
|
346
|
+
plugins/auth/service.ts
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
See [Code Organization Guide](./code-organization.md) for detailed rules on when and how to split.
|
|
350
|
+
|
|
340
351
|
### DON'T: Edit Generated Files
|
|
341
352
|
|
|
342
353
|
```ts
|
|
@@ -484,7 +495,7 @@ When adding a new feature:
|
|
|
484
495
|
1. **Putting business logic in route handlers** - Use plugin services
|
|
485
496
|
2. **Not using Zod validation** - Always validate input
|
|
486
497
|
3. **Editing generated files** - Regenerate instead
|
|
487
|
-
4. **
|
|
498
|
+
4. **Wrong file granularity** - Split at 200 lines, see [Code Organization Guide](./code-organization.md)
|
|
488
499
|
5. **Not running gen:registry** - Do this after any plugin change
|
|
489
500
|
6. **Manual auth/rate limit checks** - Use middleware
|
|
490
501
|
7. **Console.log for logging** - Use `ctx.core.logger`
|