@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.
- package/cli/donkeylabs +6 -0
- package/context.d.ts +17 -0
- package/docs/api-client.md +520 -0
- package/docs/cache.md +437 -0
- package/docs/cli.md +353 -0
- package/docs/core-services.md +338 -0
- package/docs/cron.md +465 -0
- package/docs/errors.md +303 -0
- package/docs/events.md +460 -0
- package/docs/handlers.md +549 -0
- package/docs/jobs.md +556 -0
- package/docs/logger.md +316 -0
- package/docs/middleware.md +682 -0
- package/docs/plugins.md +524 -0
- package/docs/project-structure.md +493 -0
- package/docs/rate-limiter.md +525 -0
- package/docs/router.md +566 -0
- package/docs/sse.md +542 -0
- package/docs/svelte-frontend.md +324 -0
- package/package.json +12 -9
- package/registry.d.ts +11 -0
- package/src/index.ts +1 -1
- package/src/server.ts +1 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
# Middleware
|
|
2
|
+
|
|
3
|
+
Request/response middleware for cross-cutting concerns like authentication, rate limiting, CORS, and logging.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createMiddleware } from "./middleware";
|
|
9
|
+
|
|
10
|
+
// Create middleware with config
|
|
11
|
+
const authMiddleware = createMiddleware<{ required: boolean }>(
|
|
12
|
+
async (req, ctx, next, config) => {
|
|
13
|
+
const token = req.headers.get("Authorization");
|
|
14
|
+
|
|
15
|
+
if (config.required && !token) {
|
|
16
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Modify context
|
|
20
|
+
ctx.user = await validateToken(token);
|
|
21
|
+
|
|
22
|
+
// Continue to next middleware/handler
|
|
23
|
+
return next();
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## API Reference
|
|
31
|
+
|
|
32
|
+
### Types
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// The next function - calls the next middleware or handler
|
|
36
|
+
type NextFn = () => Promise<Response>;
|
|
37
|
+
|
|
38
|
+
// Middleware function signature
|
|
39
|
+
type MiddlewareFn<TConfig = void> = (
|
|
40
|
+
req: Request,
|
|
41
|
+
ctx: ServerContext,
|
|
42
|
+
next: NextFn,
|
|
43
|
+
config: TConfig
|
|
44
|
+
) => Promise<Response>;
|
|
45
|
+
|
|
46
|
+
// Runtime middleware structure
|
|
47
|
+
interface MiddlewareRuntime<TConfig = void> {
|
|
48
|
+
execute: MiddlewareFn<TConfig>;
|
|
49
|
+
readonly __config: TConfig; // Phantom type for config inference
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### createMiddleware Factory
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
function createMiddleware<TConfig = void>(
|
|
57
|
+
execute: MiddlewareFn<TConfig>
|
|
58
|
+
): MiddlewareRuntime<TConfig>;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Creating Middleware
|
|
64
|
+
|
|
65
|
+
### Step 1: Define Middleware
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// middleware/auth.ts
|
|
69
|
+
import { createMiddleware } from "../middleware";
|
|
70
|
+
|
|
71
|
+
interface AuthConfig {
|
|
72
|
+
required?: boolean;
|
|
73
|
+
role?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const authMiddleware = createMiddleware<AuthConfig>(
|
|
77
|
+
async (req, ctx, next, config) => {
|
|
78
|
+
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
|
|
79
|
+
|
|
80
|
+
if (!token) {
|
|
81
|
+
if (config.required) {
|
|
82
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
83
|
+
}
|
|
84
|
+
return next();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const user = await verifyToken(token);
|
|
89
|
+
|
|
90
|
+
if (config.role && user.role !== config.role) {
|
|
91
|
+
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ctx.user = user;
|
|
95
|
+
} catch {
|
|
96
|
+
if (config.required) {
|
|
97
|
+
return Response.json({ error: "Invalid token" }, { status: 401 });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return next();
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Step 2: Register in Plugin
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// plugins/auth/index.ts
|
|
110
|
+
import { createPlugin } from "../../core";
|
|
111
|
+
import { authMiddleware } from "./middleware";
|
|
112
|
+
|
|
113
|
+
export const authPlugin = createPlugin.define({
|
|
114
|
+
name: "auth",
|
|
115
|
+
middleware: {
|
|
116
|
+
auth: authMiddleware, // Key becomes method name
|
|
117
|
+
},
|
|
118
|
+
service: async (ctx) => ({
|
|
119
|
+
// Auth service methods...
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Step 3: Regenerate Registry
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
bun run gen:registry
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Step 4: Use in Routes
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// Now available as .auth() method
|
|
134
|
+
router.middleware
|
|
135
|
+
.auth({ required: true, role: "admin" })
|
|
136
|
+
.route("admin").typed({ ... });
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Middleware Examples
|
|
142
|
+
|
|
143
|
+
### Rate Limiting Middleware
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
interface RateLimitConfig {
|
|
147
|
+
limit: number;
|
|
148
|
+
window: string; // "1m", "1h", etc.
|
|
149
|
+
keyPrefix?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const rateLimitMiddleware = createMiddleware<RateLimitConfig>(
|
|
153
|
+
async (req, ctx, next, config) => {
|
|
154
|
+
const windowMs = parseDuration(config.window);
|
|
155
|
+
const key = config.keyPrefix
|
|
156
|
+
? `${config.keyPrefix}:${ctx.ip}`
|
|
157
|
+
: `ratelimit:${ctx.ip}`;
|
|
158
|
+
|
|
159
|
+
const result = await ctx.core.rateLimiter.check(key, config.limit, windowMs);
|
|
160
|
+
|
|
161
|
+
if (!result.allowed) {
|
|
162
|
+
return Response.json(
|
|
163
|
+
{ error: "Rate limit exceeded" },
|
|
164
|
+
{
|
|
165
|
+
status: 429,
|
|
166
|
+
headers: { "Retry-After": String(result.retryAfter) },
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const response = await next();
|
|
172
|
+
|
|
173
|
+
// Add rate limit headers
|
|
174
|
+
response.headers.set("X-RateLimit-Limit", String(config.limit));
|
|
175
|
+
response.headers.set("X-RateLimit-Remaining", String(result.remaining));
|
|
176
|
+
|
|
177
|
+
return response;
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### CORS Middleware
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
interface CORSConfig {
|
|
186
|
+
origin?: string | string[];
|
|
187
|
+
methods?: string[];
|
|
188
|
+
headers?: string[];
|
|
189
|
+
credentials?: boolean;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const corsMiddleware = createMiddleware<CORSConfig>(
|
|
193
|
+
async (req, ctx, next, config) => {
|
|
194
|
+
const origin = req.headers.get("Origin");
|
|
195
|
+
const allowedOrigins = Array.isArray(config.origin)
|
|
196
|
+
? config.origin
|
|
197
|
+
: [config.origin || "*"];
|
|
198
|
+
|
|
199
|
+
// Handle preflight
|
|
200
|
+
if (req.method === "OPTIONS") {
|
|
201
|
+
return new Response(null, {
|
|
202
|
+
status: 204,
|
|
203
|
+
headers: {
|
|
204
|
+
"Access-Control-Allow-Origin": allowedOrigins.includes(origin!)
|
|
205
|
+
? origin!
|
|
206
|
+
: allowedOrigins[0],
|
|
207
|
+
"Access-Control-Allow-Methods": (config.methods || ["GET", "POST"]).join(", "),
|
|
208
|
+
"Access-Control-Allow-Headers": (config.headers || ["Content-Type"]).join(", "),
|
|
209
|
+
"Access-Control-Allow-Credentials": String(config.credentials ?? false),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const response = await next();
|
|
215
|
+
|
|
216
|
+
// Add CORS headers to response
|
|
217
|
+
if (origin && (allowedOrigins.includes("*") || allowedOrigins.includes(origin))) {
|
|
218
|
+
response.headers.set("Access-Control-Allow-Origin", origin);
|
|
219
|
+
if (config.credentials) {
|
|
220
|
+
response.headers.set("Access-Control-Allow-Credentials", "true");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return response;
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Logging Middleware
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
interface LogConfig {
|
|
233
|
+
level?: "debug" | "info";
|
|
234
|
+
includeBody?: boolean;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export const loggingMiddleware = createMiddleware<LogConfig>(
|
|
238
|
+
async (req, ctx, next, config) => {
|
|
239
|
+
const start = Date.now();
|
|
240
|
+
|
|
241
|
+
const logData: any = {
|
|
242
|
+
method: req.method,
|
|
243
|
+
url: req.url,
|
|
244
|
+
ip: ctx.ip,
|
|
245
|
+
requestId: ctx.requestId,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
if (config.includeBody && req.method === "POST") {
|
|
249
|
+
try {
|
|
250
|
+
logData.body = await req.clone().json();
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const log = config.level === "debug"
|
|
255
|
+
? ctx.core.logger.debug
|
|
256
|
+
: ctx.core.logger.info;
|
|
257
|
+
|
|
258
|
+
log("Request started", logData);
|
|
259
|
+
|
|
260
|
+
const response = await next();
|
|
261
|
+
|
|
262
|
+
log("Request completed", {
|
|
263
|
+
...logData,
|
|
264
|
+
status: response.status,
|
|
265
|
+
duration: Date.now() - start,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return response;
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Caching Middleware
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
interface CacheConfig {
|
|
277
|
+
ttl: number;
|
|
278
|
+
keyFn?: (req: Request, ctx: ServerContext) => string;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export const cacheMiddleware = createMiddleware<CacheConfig>(
|
|
282
|
+
async (req, ctx, next, config) => {
|
|
283
|
+
// Only cache GET requests
|
|
284
|
+
if (req.method !== "GET" && req.method !== "POST") {
|
|
285
|
+
return next();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const key = config.keyFn
|
|
289
|
+
? config.keyFn(req, ctx)
|
|
290
|
+
: `cache:${new URL(req.url).pathname}`;
|
|
291
|
+
|
|
292
|
+
// Check cache
|
|
293
|
+
const cached = await ctx.core.cache.get<{ body: string; headers: Record<string, string> }>(key);
|
|
294
|
+
|
|
295
|
+
if (cached) {
|
|
296
|
+
return new Response(cached.body, {
|
|
297
|
+
headers: {
|
|
298
|
+
...cached.headers,
|
|
299
|
+
"X-Cache": "HIT",
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Get fresh response
|
|
305
|
+
const response = await next();
|
|
306
|
+
|
|
307
|
+
// Cache the response
|
|
308
|
+
if (response.ok) {
|
|
309
|
+
const body = await response.clone().text();
|
|
310
|
+
const headers: Record<string, string> = {};
|
|
311
|
+
response.headers.forEach((v, k) => (headers[k] = v));
|
|
312
|
+
|
|
313
|
+
await ctx.core.cache.set(key, { body, headers }, config.ttl);
|
|
314
|
+
response.headers.set("X-Cache", "MISS");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return response;
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Validation Middleware
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { z } from "zod";
|
|
326
|
+
|
|
327
|
+
interface ValidationConfig {
|
|
328
|
+
headers?: z.ZodType;
|
|
329
|
+
query?: z.ZodType;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export const validateMiddleware = createMiddleware<ValidationConfig>(
|
|
333
|
+
async (req, ctx, next, config) => {
|
|
334
|
+
// Validate headers
|
|
335
|
+
if (config.headers) {
|
|
336
|
+
const headers: Record<string, string> = {};
|
|
337
|
+
req.headers.forEach((v, k) => (headers[k] = v));
|
|
338
|
+
|
|
339
|
+
const result = config.headers.safeParse(headers);
|
|
340
|
+
if (!result.success) {
|
|
341
|
+
return Response.json(
|
|
342
|
+
{ error: "Invalid headers", details: result.error.issues },
|
|
343
|
+
{ status: 400 }
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Validate query params
|
|
349
|
+
if (config.query) {
|
|
350
|
+
const url = new URL(req.url);
|
|
351
|
+
const query: Record<string, string> = {};
|
|
352
|
+
url.searchParams.forEach((v, k) => (query[k] = v));
|
|
353
|
+
|
|
354
|
+
const result = config.query.safeParse(query);
|
|
355
|
+
if (!result.success) {
|
|
356
|
+
return Response.json(
|
|
357
|
+
{ error: "Invalid query parameters", details: result.error.issues },
|
|
358
|
+
{ status: 400 }
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return next();
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Using Middleware
|
|
371
|
+
|
|
372
|
+
### Single Middleware
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
router.middleware
|
|
376
|
+
.auth({ required: true })
|
|
377
|
+
.route("protected").typed({
|
|
378
|
+
handle: async (input, ctx) => {
|
|
379
|
+
return { userId: ctx.user.id };
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Chained Middleware
|
|
385
|
+
|
|
386
|
+
Middleware executes in order (left to right):
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
router.middleware
|
|
390
|
+
.cors({ origin: "*" }) // 1st: CORS handling
|
|
391
|
+
.logging({ level: "info" }) // 2nd: Log request start
|
|
392
|
+
.auth({ required: true }) // 3rd: Check authentication
|
|
393
|
+
.rateLimit({ limit: 100, window: "1m" }) // 4th: Rate limiting
|
|
394
|
+
.route("api").typed({
|
|
395
|
+
handle: async (input, ctx) => {
|
|
396
|
+
// All middleware passed
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Reusable Middleware Chain
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
// Create reusable middleware chain
|
|
405
|
+
const protectedApi = router.middleware
|
|
406
|
+
.cors({ origin: "https://myapp.com" })
|
|
407
|
+
.auth({ required: true })
|
|
408
|
+
.rateLimit({ limit: 1000, window: "1h" });
|
|
409
|
+
|
|
410
|
+
// Apply to multiple routes
|
|
411
|
+
protectedApi.route("users").typed({ ... });
|
|
412
|
+
protectedApi.route("orders").typed({ ... });
|
|
413
|
+
protectedApi.route("products").typed({ ... });
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Conditional Middleware
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
const baseMiddleware = router.middleware.cors({ origin: "*" });
|
|
420
|
+
|
|
421
|
+
// Add auth only in production
|
|
422
|
+
const middleware = process.env.NODE_ENV === "production"
|
|
423
|
+
? baseMiddleware.auth({ required: true })
|
|
424
|
+
: baseMiddleware;
|
|
425
|
+
|
|
426
|
+
middleware.route("api").typed({ ... });
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Middleware Execution Flow
|
|
432
|
+
|
|
433
|
+
```
|
|
434
|
+
Request
|
|
435
|
+
│
|
|
436
|
+
▼
|
|
437
|
+
┌─────────────────────────────────┐
|
|
438
|
+
│ Middleware 1 (CORS) │
|
|
439
|
+
│ │ │
|
|
440
|
+
│ ▼ │
|
|
441
|
+
│ Middleware 2 (Auth) │
|
|
442
|
+
│ │ │
|
|
443
|
+
│ ▼ │
|
|
444
|
+
│ Middleware 3 (RateLimit) │
|
|
445
|
+
│ │ │
|
|
446
|
+
│ ▼ │
|
|
447
|
+
│ ┌───────────────────────────┐ │
|
|
448
|
+
│ │ Handler │ │
|
|
449
|
+
│ │ (your route handler) │ │
|
|
450
|
+
│ └───────────────────────────┘ │
|
|
451
|
+
│ │ │
|
|
452
|
+
│ ▼ │
|
|
453
|
+
│ Middleware 3 (post-handler) │
|
|
454
|
+
│ │ │
|
|
455
|
+
│ ▼ │
|
|
456
|
+
│ Middleware 2 (post-handler) │
|
|
457
|
+
│ │ │
|
|
458
|
+
│ ▼ │
|
|
459
|
+
│ Middleware 1 (post-handler) │
|
|
460
|
+
└─────────────────────────────────┘
|
|
461
|
+
│
|
|
462
|
+
▼
|
|
463
|
+
Response
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Modifying Context
|
|
469
|
+
|
|
470
|
+
Middleware can add properties to `ctx`:
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
// Auth middleware adds ctx.user
|
|
474
|
+
const authMiddleware = createMiddleware<AuthConfig>(async (req, ctx, next, config) => {
|
|
475
|
+
ctx.user = await validateToken(req.headers.get("Authorization"));
|
|
476
|
+
return next();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Handler can access ctx.user
|
|
480
|
+
router.middleware.auth({ required: true }).route("profile").typed({
|
|
481
|
+
handle: async (input, ctx) => {
|
|
482
|
+
return { name: ctx.user.name }; // ctx.user is set
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
## Response Modification
|
|
490
|
+
|
|
491
|
+
Middleware can modify the response:
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
const timingMiddleware = createMiddleware(async (req, ctx, next) => {
|
|
495
|
+
const start = Date.now();
|
|
496
|
+
|
|
497
|
+
// Get response from handler
|
|
498
|
+
const response = await next();
|
|
499
|
+
|
|
500
|
+
// Add timing header
|
|
501
|
+
response.headers.set("X-Response-Time", `${Date.now() - start}ms`);
|
|
502
|
+
|
|
503
|
+
return response;
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## Early Returns
|
|
510
|
+
|
|
511
|
+
Middleware can return early without calling `next()`:
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
const maintenanceMiddleware = createMiddleware(async (req, ctx, next) => {
|
|
515
|
+
if (process.env.MAINTENANCE_MODE === "true") {
|
|
516
|
+
return Response.json(
|
|
517
|
+
{ error: "Service under maintenance" },
|
|
518
|
+
{ status: 503 }
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return next(); // Continue if not in maintenance
|
|
523
|
+
});
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Error Handling
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
const errorMiddleware = createMiddleware(async (req, ctx, next) => {
|
|
532
|
+
try {
|
|
533
|
+
return await next();
|
|
534
|
+
} catch (error: any) {
|
|
535
|
+
ctx.core.logger.error("Unhandled error", {
|
|
536
|
+
error: error.message,
|
|
537
|
+
stack: error.stack,
|
|
538
|
+
requestId: ctx.requestId,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
return Response.json(
|
|
542
|
+
{ error: "Internal server error", requestId: ctx.requestId },
|
|
543
|
+
{ status: 500 }
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Best Practices
|
|
552
|
+
|
|
553
|
+
### 1. Keep Middleware Focused
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
// Good - single responsibility
|
|
557
|
+
const authMiddleware = createMiddleware(...); // Just auth
|
|
558
|
+
const rateLimitMiddleware = createMiddleware(...); // Just rate limiting
|
|
559
|
+
const loggingMiddleware = createMiddleware(...); // Just logging
|
|
560
|
+
|
|
561
|
+
// Bad - too many responsibilities
|
|
562
|
+
const everythingMiddleware = createMiddleware(async (req, ctx, next) => {
|
|
563
|
+
// Check auth
|
|
564
|
+
// Rate limit
|
|
565
|
+
// Log
|
|
566
|
+
// Validate
|
|
567
|
+
// Cache
|
|
568
|
+
// ...
|
|
569
|
+
});
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### 2. Order Matters
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
// Good order
|
|
576
|
+
router.middleware
|
|
577
|
+
.cors() // Handle CORS first (for preflight)
|
|
578
|
+
.logging() // Log all requests
|
|
579
|
+
.auth() // Then authenticate
|
|
580
|
+
.rateLimit() // Then rate limit
|
|
581
|
+
.route("api")
|
|
582
|
+
|
|
583
|
+
// Bad order
|
|
584
|
+
router.middleware
|
|
585
|
+
.rateLimit() // Rate limit before auth = limit by IP only
|
|
586
|
+
.auth() // Auth after rate limit = may waste rate limit on bad tokens
|
|
587
|
+
.cors() // CORS late = preflight requests fail
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### 3. Make Config Optional When Possible
|
|
591
|
+
|
|
592
|
+
```ts
|
|
593
|
+
interface AuthConfig {
|
|
594
|
+
required?: boolean; // Default: false
|
|
595
|
+
role?: string; // Default: any role
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Allows simple usage
|
|
599
|
+
router.middleware.auth().route("optional-auth")
|
|
600
|
+
router.middleware.auth({ required: true }).route("required-auth")
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### 4. Document Configuration
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
/**
|
|
607
|
+
* Rate limiting middleware
|
|
608
|
+
*
|
|
609
|
+
* @param config.limit - Max requests in window (default: 100)
|
|
610
|
+
* @param config.window - Time window ("1m", "1h", "1d")
|
|
611
|
+
* @param config.keyPrefix - Cache key prefix (default: "ratelimit")
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* router.middleware.rateLimit({ limit: 100, window: "1m" })
|
|
615
|
+
*/
|
|
616
|
+
export const rateLimitMiddleware = createMiddleware<RateLimitConfig>(...);
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### 5. Test Middleware in Isolation
|
|
620
|
+
|
|
621
|
+
```ts
|
|
622
|
+
import { authMiddleware } from "./middleware/auth";
|
|
623
|
+
|
|
624
|
+
test("auth middleware rejects invalid token", async () => {
|
|
625
|
+
const req = new Request("http://test", {
|
|
626
|
+
headers: { Authorization: "Bearer invalid" },
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const ctx = createMockContext();
|
|
630
|
+
const next = vi.fn();
|
|
631
|
+
|
|
632
|
+
const response = await authMiddleware.execute(req, ctx, next, { required: true });
|
|
633
|
+
|
|
634
|
+
expect(response.status).toBe(401);
|
|
635
|
+
expect(next).not.toHaveBeenCalled();
|
|
636
|
+
});
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Common Patterns
|
|
642
|
+
|
|
643
|
+
### Authentication + Authorization
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
// First check if authenticated, then check role
|
|
647
|
+
router.middleware
|
|
648
|
+
.auth({ required: true })
|
|
649
|
+
.role({ allowed: ["admin", "moderator"] })
|
|
650
|
+
.route("admin").typed({ ... });
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### Public Routes with Optional Auth
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
// Auth runs but doesn't require login
|
|
657
|
+
router.middleware
|
|
658
|
+
.auth({ required: false })
|
|
659
|
+
.route("public").typed({
|
|
660
|
+
handle: async (input, ctx) => {
|
|
661
|
+
// ctx.user may or may not exist
|
|
662
|
+
if (ctx.user) {
|
|
663
|
+
return { message: `Hello, ${ctx.user.name}!` };
|
|
664
|
+
}
|
|
665
|
+
return { message: "Hello, guest!" };
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### Environment-Specific Middleware
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
const middleware = router.middleware.cors({ origin: "*" });
|
|
674
|
+
|
|
675
|
+
if (process.env.NODE_ENV === "production") {
|
|
676
|
+
middleware
|
|
677
|
+
.auth({ required: true })
|
|
678
|
+
.rateLimit({ limit: 100, window: "1m" });
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
middleware.route("api").typed({ ... });
|
|
682
|
+
```
|