@donkeylabs/server 0.1.1 → 0.1.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.
@@ -0,0 +1,549 @@
1
+ # Handlers
2
+
3
+ Request handlers define how routes process HTTP requests. Built-in handlers cover common cases, and you can create custom handlers for specialized needs.
4
+
5
+ ## Quick Start
6
+
7
+ ```ts
8
+ import { createHandler } from "./handlers";
9
+ import type { ServerContext } from "./router";
10
+
11
+ // Define handler function signature
12
+ type MyFn = (data: MyInput, ctx: ServerContext) => Promise<MyOutput>;
13
+
14
+ // Create custom handler
15
+ export const MyHandler = createHandler<MyFn>(async (req, def, handle, ctx) => {
16
+ const data = await req.json();
17
+ const result = await handle(data, ctx);
18
+ return Response.json(result);
19
+ });
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Built-in Handlers
25
+
26
+ ### TypedHandler (Default)
27
+
28
+ JSON-RPC style handler with automatic validation:
29
+
30
+ ```ts
31
+ router.route("greet").typed({
32
+ input: z.object({ name: z.string() }),
33
+ output: z.object({ message: z.string() }),
34
+ handle: async (input, ctx) => {
35
+ return { message: `Hello, ${input.name}!` };
36
+ },
37
+ });
38
+ ```
39
+
40
+ **Behavior:**
41
+ - Accepts POST requests only (405 for others)
42
+ - Parses JSON body
43
+ - Validates input with Zod schema (if provided)
44
+ - Calls your handler with parsed input
45
+ - Validates output with Zod schema (if provided)
46
+ - Returns JSON response
47
+
48
+ **Error Responses:**
49
+
50
+ | Status | Condition |
51
+ |--------|-----------|
52
+ | 405 | Non-POST request |
53
+ | 400 | Invalid JSON body |
54
+ | 400 | Input validation failed |
55
+ | 500 | Handler threw error |
56
+
57
+ ### RawHandler
58
+
59
+ Full control over Request and Response:
60
+
61
+ ```ts
62
+ router.route("upload").raw({
63
+ handle: async (req, ctx) => {
64
+ if (req.method !== "POST") {
65
+ return new Response("Method Not Allowed", { status: 405 });
66
+ }
67
+
68
+ const formData = await req.formData();
69
+ const file = formData.get("file") as File;
70
+
71
+ // Process file...
72
+
73
+ return Response.json({ success: true });
74
+ },
75
+ });
76
+ ```
77
+
78
+ **Use cases:**
79
+ - File uploads/downloads
80
+ - Streaming responses
81
+ - Server-Sent Events
82
+ - Custom content types
83
+ - WebSocket upgrades
84
+ - Non-JSON APIs
85
+
86
+ ---
87
+
88
+ ## API Reference
89
+
90
+ ### HandlerRuntime Interface
91
+
92
+ All handlers implement this interface:
93
+
94
+ ```ts
95
+ interface HandlerRuntime<Fn extends Function = Function> {
96
+ execute(
97
+ req: Request,
98
+ def: RouteDefinition,
99
+ userHandle: Fn,
100
+ ctx: ServerContext
101
+ ): Promise<Response>;
102
+
103
+ readonly __signature: Fn; // Required for type inference
104
+ }
105
+ ```
106
+
107
+ ### createHandler Factory
108
+
109
+ Create custom handlers without manual phantom types:
110
+
111
+ ```ts
112
+ function createHandler<Fn extends Function>(
113
+ execute: (
114
+ req: Request,
115
+ def: RouteDefinition,
116
+ handle: Fn,
117
+ ctx: ServerContext
118
+ ) => Promise<Response>
119
+ ): HandlerRuntime<Fn>;
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Creating Custom Handlers
125
+
126
+ ### Step 1: Define Function Signature
127
+
128
+ ```ts
129
+ // The signature your handler users will implement
130
+ type EchoFn = (body: any, ctx: ServerContext) => Promise<{ echo: any }>;
131
+ ```
132
+
133
+ ### Step 2: Create Handler
134
+
135
+ ```ts
136
+ import { createHandler } from "./handlers";
137
+
138
+ export const EchoHandler = createHandler<EchoFn>(async (req, def, handle, ctx) => {
139
+ // 1. Process the request
140
+ const body = await req.json();
141
+
142
+ // 2. Call the user's handler
143
+ const result = await handle(body, ctx);
144
+
145
+ // 3. Return the response
146
+ return Response.json(result);
147
+ });
148
+ ```
149
+
150
+ ### Step 3: Register in Plugin
151
+
152
+ ```ts
153
+ import { createPlugin } from "./core";
154
+ import { EchoHandler } from "./handlers/echo";
155
+
156
+ export const echoPlugin = createPlugin.define({
157
+ name: "echo",
158
+ handlers: {
159
+ echo: EchoHandler, // Key becomes method name
160
+ },
161
+ service: async () => ({}),
162
+ });
163
+ ```
164
+
165
+ ### Step 4: Regenerate Registry
166
+
167
+ ```sh
168
+ bun run gen:registry
169
+ ```
170
+
171
+ ### Step 5: Use in Routes
172
+
173
+ ```ts
174
+ // Now available as .echo() method
175
+ router.route("test").echo({
176
+ handle: async (body, ctx) => {
177
+ return { echo: body };
178
+ },
179
+ });
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Custom Handler Examples
185
+
186
+ ### XML Handler
187
+
188
+ Accept and return XML:
189
+
190
+ ```ts
191
+ import { createHandler } from "./handlers";
192
+ import { parseXML, buildXML } from "./utils/xml";
193
+
194
+ type XMLFn = (data: object, ctx: ServerContext) => Promise<object>;
195
+
196
+ export const XMLHandler = createHandler<XMLFn>(async (req, def, handle, ctx) => {
197
+ // Parse XML body
198
+ const xmlText = await req.text();
199
+ const data = parseXML(xmlText);
200
+
201
+ // Call user handler
202
+ const result = await handle(data, ctx);
203
+
204
+ // Return XML response
205
+ return new Response(buildXML(result), {
206
+ headers: { "Content-Type": "application/xml" },
207
+ });
208
+ });
209
+ ```
210
+
211
+ ### Form Handler
212
+
213
+ Process form submissions:
214
+
215
+ ```ts
216
+ type FormFn = (fields: Record<string, string>, ctx: ServerContext) => Promise<any>;
217
+
218
+ export const FormHandler = createHandler<FormFn>(async (req, def, handle, ctx) => {
219
+ const formData = await req.formData();
220
+ const fields: Record<string, string> = {};
221
+
222
+ for (const [key, value] of formData.entries()) {
223
+ if (typeof value === "string") {
224
+ fields[key] = value;
225
+ }
226
+ }
227
+
228
+ const result = await handle(fields, ctx);
229
+ return Response.json(result);
230
+ });
231
+ ```
232
+
233
+ ### GraphQL-Style Handler
234
+
235
+ Single endpoint with operation selection:
236
+
237
+ ```ts
238
+ type GraphQLFn = (
239
+ query: string,
240
+ variables: Record<string, any>,
241
+ ctx: ServerContext
242
+ ) => Promise<{ data?: any; errors?: any[] }>;
243
+
244
+ export const GraphQLHandler = createHandler<GraphQLFn>(async (req, def, handle, ctx) => {
245
+ const body = await req.json();
246
+ const { query, variables = {} } = body;
247
+
248
+ if (!query) {
249
+ return Response.json({ errors: [{ message: "Query required" }] }, { status: 400 });
250
+ }
251
+
252
+ const result = await handle(query, variables, ctx);
253
+ return Response.json(result);
254
+ });
255
+ ```
256
+
257
+ ### Streaming Handler
258
+
259
+ Support streaming responses:
260
+
261
+ ```ts
262
+ type StreamFn = (
263
+ input: any,
264
+ ctx: ServerContext
265
+ ) => AsyncGenerator<string, void, unknown>;
266
+
267
+ export const StreamHandler = createHandler<StreamFn>(async (req, def, handle, ctx) => {
268
+ const input = await req.json();
269
+
270
+ const stream = new ReadableStream({
271
+ async start(controller) {
272
+ const encoder = new TextEncoder();
273
+
274
+ for await (const chunk of handle(input, ctx)) {
275
+ controller.enqueue(encoder.encode(chunk));
276
+ }
277
+
278
+ controller.close();
279
+ },
280
+ });
281
+
282
+ return new Response(stream, {
283
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
284
+ });
285
+ });
286
+ ```
287
+
288
+ Usage:
289
+
290
+ ```ts
291
+ router.route("stream").stream({
292
+ handle: async function* (input, ctx) {
293
+ for (let i = 0; i < 10; i++) {
294
+ yield `Chunk ${i}\n`;
295
+ await new Promise((r) => setTimeout(r, 100));
296
+ }
297
+ },
298
+ });
299
+ ```
300
+
301
+ ### Batch Handler
302
+
303
+ Process multiple operations in one request:
304
+
305
+ ```ts
306
+ type BatchFn = (
307
+ operations: { id: string; method: string; params: any }[],
308
+ ctx: ServerContext
309
+ ) => Promise<{ id: string; result?: any; error?: string }[]>;
310
+
311
+ export const BatchHandler = createHandler<BatchFn>(async (req, def, handle, ctx) => {
312
+ const body = await req.json();
313
+
314
+ if (!Array.isArray(body)) {
315
+ return Response.json({ error: "Expected array" }, { status: 400 });
316
+ }
317
+
318
+ const results = await handle(body, ctx);
319
+ return Response.json(results);
320
+ });
321
+ ```
322
+
323
+ ---
324
+
325
+ ## Handler Configuration
326
+
327
+ Custom handlers can access route configuration:
328
+
329
+ ```ts
330
+ // Route definition includes custom config
331
+ router.route("cached").typed({
332
+ input: z.object({ id: z.string() }),
333
+ cache: { ttl: 60000 }, // Custom config
334
+ handle: async (input, ctx) => { ... },
335
+ });
336
+ ```
337
+
338
+ Access in handler:
339
+
340
+ ```ts
341
+ export const CachedHandler = createHandler<CachedFn>(async (req, def, handle, ctx) => {
342
+ const cacheConfig = (def as any).cache;
343
+
344
+ if (cacheConfig) {
345
+ const cached = await ctx.core.cache.get(cacheKey);
346
+ if (cached) return Response.json(cached);
347
+ }
348
+
349
+ const result = await handle(input, ctx);
350
+
351
+ if (cacheConfig) {
352
+ await ctx.core.cache.set(cacheKey, result, cacheConfig.ttl);
353
+ }
354
+
355
+ return Response.json(result);
356
+ });
357
+ ```
358
+
359
+ ---
360
+
361
+ ## Error Handling
362
+
363
+ ### In Custom Handlers
364
+
365
+ ```ts
366
+ export const SafeHandler = createHandler<SafeFn>(async (req, def, handle, ctx) => {
367
+ try {
368
+ const body = await req.json();
369
+ const result = await handle(body, ctx);
370
+ return Response.json(result);
371
+ } catch (error: any) {
372
+ ctx.core.logger.error("Handler error", { error: error.message });
373
+
374
+ if (error.name === "ValidationError") {
375
+ return Response.json({ error: error.message }, { status: 400 });
376
+ }
377
+
378
+ return Response.json({ error: "Internal server error" }, { status: 500 });
379
+ }
380
+ });
381
+ ```
382
+
383
+ ### Zod Validation Errors
384
+
385
+ TypedHandler returns structured validation errors:
386
+
387
+ ```json
388
+ {
389
+ "error": "Validation Failed",
390
+ "details": [
391
+ {
392
+ "path": ["email"],
393
+ "message": "Invalid email",
394
+ "code": "invalid_string"
395
+ },
396
+ {
397
+ "path": ["age"],
398
+ "message": "Expected number, received string",
399
+ "code": "invalid_type"
400
+ }
401
+ ]
402
+ }
403
+ ```
404
+
405
+ ---
406
+
407
+ ## Handler Resolution
408
+
409
+ The server resolves handlers at runtime:
410
+
411
+ 1. Route specifies handler name (e.g., `"typed"`, `"raw"`, `"echo"`)
412
+ 2. Server looks up handler in merged registry (built-in + plugin handlers)
413
+ 3. Handler's `execute()` method is called with request, definition, user handle, and context
414
+
415
+ ```ts
416
+ // In server.ts (simplified)
417
+ const handler = handlers[route.handler];
418
+ const response = await handler.execute(req, route, route.handle, ctx);
419
+ ```
420
+
421
+ ---
422
+
423
+ ## TypeScript Integration
424
+
425
+ ### Handler Type Inference
426
+
427
+ The `__signature` phantom type enables autocomplete:
428
+
429
+ ```ts
430
+ // When you type: router.route("test").echo({
431
+ // handle: ... <-- TypeScript knows this should be EchoFn
432
+ // });
433
+
434
+ interface HandlerRuntime<Fn extends Function> {
435
+ readonly __signature: Fn; // This enables inference
436
+ }
437
+ ```
438
+
439
+ ### Generating Registry
440
+
441
+ After adding handlers to a plugin, regenerate types:
442
+
443
+ ```sh
444
+ bun run gen:registry
445
+ ```
446
+
447
+ This generates `registry.d.ts` which augments `IRouteBuilder`:
448
+
449
+ ```ts
450
+ declare module "./router" {
451
+ interface IRouteBuilder<TRouter> {
452
+ echo(config: { handle: EchoFn }): TRouter;
453
+ // ... other handlers
454
+ }
455
+ }
456
+ ```
457
+
458
+ ---
459
+
460
+ ## Best Practices
461
+
462
+ ### 1. Keep Handlers Focused
463
+
464
+ ```ts
465
+ // Good - single responsibility
466
+ export const JSONHandler = createHandler<JSONFn>(async (req, def, handle, ctx) => {
467
+ const body = await req.json();
468
+ const result = await handle(body, ctx);
469
+ return Response.json(result);
470
+ });
471
+
472
+ // Bad - too many concerns
473
+ export const EverythingHandler = createHandler<Fn>(async (req, def, handle, ctx) => {
474
+ // Auth check
475
+ // Rate limiting
476
+ // Caching
477
+ // Logging
478
+ // Validation
479
+ // Error handling
480
+ // ...
481
+ });
482
+ ```
483
+
484
+ Use middleware for cross-cutting concerns.
485
+
486
+ ### 2. Validate Input Early
487
+
488
+ ```ts
489
+ export const SafeHandler = createHandler<SafeFn>(async (req, def, handle, ctx) => {
490
+ // Validate before calling user handler
491
+ let body;
492
+ try {
493
+ body = await req.json();
494
+ } catch {
495
+ return Response.json({ error: "Invalid JSON" }, { status: 400 });
496
+ }
497
+
498
+ const result = await handle(body, ctx);
499
+ return Response.json(result);
500
+ });
501
+ ```
502
+
503
+ ### 3. Use Proper Error Responses
504
+
505
+ ```ts
506
+ // Good - proper HTTP status codes
507
+ return Response.json({ error: "Not found" }, { status: 404 });
508
+ return Response.json({ error: "Validation failed" }, { status: 400 });
509
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
510
+
511
+ // Bad - always 200 with error in body
512
+ return Response.json({ success: false, error: "Not found" });
513
+ ```
514
+
515
+ ### 4. Document Handler Contracts
516
+
517
+ ```ts
518
+ /**
519
+ * FormHandler - Processes multipart form submissions
520
+ *
521
+ * Request: multipart/form-data with fields
522
+ * Response: JSON
523
+ *
524
+ * Handler signature:
525
+ * (fields: Record<string, string>, files: File[], ctx) => Promise<any>
526
+ */
527
+ export const FormHandler = createHandler<FormFn>(...);
528
+ ```
529
+
530
+ ### 5. Test Handlers Independently
531
+
532
+ ```ts
533
+ import { EchoHandler } from "./handlers/echo";
534
+
535
+ test("EchoHandler echoes body", async () => {
536
+ const req = new Request("http://test", {
537
+ method: "POST",
538
+ body: JSON.stringify({ hello: "world" }),
539
+ });
540
+
541
+ const mockHandle = async (body: any) => ({ echo: body });
542
+ const ctx = createMockContext();
543
+
544
+ const response = await EchoHandler.execute(req, {}, mockHandle, ctx);
545
+ const json = await response.json();
546
+
547
+ expect(json).toEqual({ echo: { hello: "world" } });
548
+ });
549
+ ```