@fresho/router 0.0.1 → 0.0.4
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/LICENSE +21 -0
- package/README.md +401 -28
- package/dist/index.d.mts +377 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1046 -0
- package/dist/index.mjs.map +1 -0
- package/dist/middleware/index.d.mts +396 -0
- package/dist/middleware/index.d.mts.map +1 -0
- package/dist/middleware/index.mjs +675 -0
- package/dist/middleware/index.mjs.map +1 -0
- package/dist/middleware-BpFh0XrR.mjs +50 -0
- package/dist/middleware-BpFh0XrR.mjs.map +1 -0
- package/dist/middleware-CqtYOc6D.d.mts +382 -0
- package/dist/middleware-CqtYOc6D.d.mts.map +1 -0
- package/docs/authentication.md +385 -0
- package/docs/middleware.md +441 -0
- package/package.json +63 -5
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# Middleware
|
|
2
|
+
|
|
3
|
+
@fresho/router provides a composable middleware system for request/response processing. Middleware can handle cross-cutting concerns like authentication, logging, CORS, and rate limiting.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Middleware Basics](#middleware-basics)
|
|
8
|
+
- [Authoring Middleware](#authoring-middleware)
|
|
9
|
+
- [Simple Middleware](#simple-middleware)
|
|
10
|
+
- [Typed Context Access](#typed-context-access)
|
|
11
|
+
- [Adding Context Properties](#adding-context-properties)
|
|
12
|
+
- [Built-in Middleware](#built-in-middleware)
|
|
13
|
+
- [Authentication](#authentication)
|
|
14
|
+
- [CORS](#cors)
|
|
15
|
+
- [Error Handling](#errorhandler)
|
|
16
|
+
- [Logging](#logger)
|
|
17
|
+
- [Rate Limiting](#ratelimit)
|
|
18
|
+
- [Request ID](#requestid)
|
|
19
|
+
- [Timeout](#timeout)
|
|
20
|
+
- [Content Type Validation](#contenttype)
|
|
21
|
+
- [Middleware Utilities](#middleware-utilities)
|
|
22
|
+
- [compose](#compose)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Middleware Basics
|
|
27
|
+
|
|
28
|
+
Middleware functions receive a context and a `next` function. They can:
|
|
29
|
+
- Modify the request before it reaches the handler
|
|
30
|
+
- Short-circuit the request (return early without calling `next`)
|
|
31
|
+
- Modify the response after the handler completes
|
|
32
|
+
- Add properties to the context for downstream middleware and handlers
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import type { Middleware } from '@fresho/router';
|
|
36
|
+
|
|
37
|
+
const timing: Middleware = async (ctx, next) => {
|
|
38
|
+
const start = Date.now();
|
|
39
|
+
const response = await next();
|
|
40
|
+
const ms = Date.now() - start;
|
|
41
|
+
response.headers.set('X-Response-Time', `${ms}ms`);
|
|
42
|
+
return response;
|
|
43
|
+
};
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Add middleware to a router after the routes:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { router } from '@fresho/router';
|
|
50
|
+
import { cors, errorHandler } from '@fresho/router/middleware';
|
|
51
|
+
|
|
52
|
+
const api = router({
|
|
53
|
+
hello: router({
|
|
54
|
+
get: async () => ({ message: 'Hello' }),
|
|
55
|
+
}),
|
|
56
|
+
}, cors(), errorHandler(), timing);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Authoring Middleware
|
|
62
|
+
|
|
63
|
+
### Simple Middleware
|
|
64
|
+
|
|
65
|
+
A middleware is a function that takes `(context, next)` and returns a `Response`:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import type { Middleware, MiddlewareContext, MiddlewareNext } from '@fresho/router';
|
|
69
|
+
|
|
70
|
+
const logger: Middleware = async (ctx: MiddlewareContext, next: MiddlewareNext) => {
|
|
71
|
+
console.log(`${ctx.request.method} ${ctx.request.url}`);
|
|
72
|
+
return next();
|
|
73
|
+
};
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Typed Context Access
|
|
77
|
+
|
|
78
|
+
Use `Middleware<Ctx>` to type the context (environment and middleware-added properties):
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
interface AppContext {
|
|
82
|
+
env: {
|
|
83
|
+
JWT_SECRET: string;
|
|
84
|
+
DB: D1Database;
|
|
85
|
+
};
|
|
86
|
+
user?: { id: string }; // Optional - may not exist yet
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const auth: Middleware<AppContext> = async (ctx, next) => {
|
|
90
|
+
// ctx.env is typed (env.JWT_SECRET, env.DB)
|
|
91
|
+
// Other context properties are Partial (user may be undefined)
|
|
92
|
+
const secret = ctx.env.JWT_SECRET;
|
|
93
|
+
ctx.user = await validateToken(ctx.request, secret);
|
|
94
|
+
return next();
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Note: In middleware, `env` is required but other context properties are `Partial` since middleware may be building them up. In handlers, all context properties are required (middleware has already run).
|
|
99
|
+
|
|
100
|
+
### Adding Context Properties
|
|
101
|
+
|
|
102
|
+
Middleware can add properties to the context. These are available to downstream middleware and route handlers:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// Middleware adds user to context
|
|
106
|
+
const auth: Middleware = async (ctx, next) => {
|
|
107
|
+
ctx.user = await authenticate(ctx.request);
|
|
108
|
+
return next();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Route handler receives the context with user
|
|
112
|
+
profile: router({
|
|
113
|
+
get: route.ctx<{ user: User }>()({
|
|
114
|
+
handler: async (c) => {
|
|
115
|
+
// c.user is typed as User
|
|
116
|
+
return { name: c.user.name };
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The `route.ctx<T>()` builder tells TypeScript what context properties the handler expects.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Built-in Middleware
|
|
127
|
+
|
|
128
|
+
Import from `@fresho/router/middleware`:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import {
|
|
132
|
+
// Authentication
|
|
133
|
+
basicAuth,
|
|
134
|
+
bearerAuth,
|
|
135
|
+
jwtAuth,
|
|
136
|
+
// CORS
|
|
137
|
+
cors,
|
|
138
|
+
// Utilities
|
|
139
|
+
errorHandler,
|
|
140
|
+
logger,
|
|
141
|
+
rateLimit,
|
|
142
|
+
requestId,
|
|
143
|
+
timeout,
|
|
144
|
+
contentType,
|
|
145
|
+
} from '@fresho/router/middleware';
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### Authentication
|
|
151
|
+
|
|
152
|
+
@fresho/router includes several authentication middleware options:
|
|
153
|
+
|
|
154
|
+
- **`basicAuth`** - HTTP Basic authentication
|
|
155
|
+
- **`bearerAuth`** - Simple bearer token authentication
|
|
156
|
+
- **`jwtAuth`** - JWT authentication with signature verification
|
|
157
|
+
- **`jwtSign`** - JWT token signing utility
|
|
158
|
+
|
|
159
|
+
For complete documentation, examples, and patterns (cookie-based auth, token refresh, etc.), see the **[Authentication Guide](authentication.md)**.
|
|
160
|
+
|
|
161
|
+
Quick example:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { jwtAuth } from '@fresho/router/middleware';
|
|
165
|
+
|
|
166
|
+
const api = router({
|
|
167
|
+
profile: router({
|
|
168
|
+
get: route.ctx<{ user: { id: string } }>()({
|
|
169
|
+
handler: async (c) => ({ id: c.user.id }),
|
|
170
|
+
}),
|
|
171
|
+
}),
|
|
172
|
+
},
|
|
173
|
+
jwtAuth({
|
|
174
|
+
secret: process.env.JWT_SECRET,
|
|
175
|
+
claims: (payload) => ({ user: { id: payload.sub } }),
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
### cors
|
|
183
|
+
|
|
184
|
+
Cross-Origin Resource Sharing headers.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
cors({
|
|
188
|
+
// Allowed origins (default: '*')
|
|
189
|
+
// Can be: string, string[], RegExp, or (origin: string) => boolean
|
|
190
|
+
origin: 'https://example.com',
|
|
191
|
+
// or: origin: ['https://example.com', 'https://app.example.com'],
|
|
192
|
+
// or: origin: /\.example\.com$/,
|
|
193
|
+
// or: origin: (origin) => origin.endsWith('.example.com'),
|
|
194
|
+
|
|
195
|
+
// Allowed methods (default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD)
|
|
196
|
+
methods: ['GET', 'POST'],
|
|
197
|
+
|
|
198
|
+
// Allowed request headers (default: Content-Type, Authorization)
|
|
199
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header'],
|
|
200
|
+
|
|
201
|
+
// Headers to expose to the browser
|
|
202
|
+
exposedHeaders: ['X-Total-Count'],
|
|
203
|
+
|
|
204
|
+
// Allow credentials (cookies, authorization headers)
|
|
205
|
+
credentials: true,
|
|
206
|
+
|
|
207
|
+
// Max age for preflight cache in seconds (default: 86400)
|
|
208
|
+
maxAge: 3600,
|
|
209
|
+
|
|
210
|
+
// Pass preflight to next handler instead of returning 204
|
|
211
|
+
preflightContinue: false,
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### errorHandler
|
|
218
|
+
|
|
219
|
+
Catches errors and returns JSON error responses.
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
errorHandler({
|
|
223
|
+
// Custom error logger
|
|
224
|
+
log: (error, ctx) => {
|
|
225
|
+
console.error(`[${ctx.requestId}]`, error);
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
// Expose error details in response (default: false)
|
|
229
|
+
// WARNING: Set to false in production
|
|
230
|
+
expose: process.env.NODE_ENV === 'development',
|
|
231
|
+
|
|
232
|
+
// Custom error response formatter
|
|
233
|
+
formatter: (error, ctx) => {
|
|
234
|
+
return new Response(JSON.stringify({
|
|
235
|
+
error: error.message,
|
|
236
|
+
requestId: ctx.requestId,
|
|
237
|
+
}), {
|
|
238
|
+
status: 500,
|
|
239
|
+
headers: { 'Content-Type': 'application/json' },
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Supports errors with `status` or `statusCode` properties for custom HTTP status codes.
|
|
246
|
+
|
|
247
|
+
**Using HttpError:**
|
|
248
|
+
|
|
249
|
+
Use the `HttpError` class to throw errors with specific status codes:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { route, router } from '@fresho/router';
|
|
253
|
+
import { errorHandler, HttpError } from '@fresho/router/middleware';
|
|
254
|
+
|
|
255
|
+
const api = router({
|
|
256
|
+
users: router({
|
|
257
|
+
$id: router({
|
|
258
|
+
get: async (c) => {
|
|
259
|
+
const user = await db.findUser(c.path.id);
|
|
260
|
+
if (!user) throw new HttpError('User not found', 404);
|
|
261
|
+
return user;
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
}),
|
|
265
|
+
}, errorHandler());
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
This keeps handler return types clean (just `User`, not `User | Response`) while still allowing custom error responses.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### logger
|
|
273
|
+
|
|
274
|
+
Request/response logging.
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
logger({
|
|
278
|
+
// Custom log function (default: console.log)
|
|
279
|
+
log: (message) => myLogger.info(message),
|
|
280
|
+
|
|
281
|
+
// Include request headers in log
|
|
282
|
+
includeHeaders: true,
|
|
283
|
+
|
|
284
|
+
// Include request body in log
|
|
285
|
+
includeBody: true,
|
|
286
|
+
|
|
287
|
+
// Custom log formatter
|
|
288
|
+
formatter: (info) => {
|
|
289
|
+
return `${info.method} ${info.url} ${info.status} ${info.duration}ms`;
|
|
290
|
+
},
|
|
291
|
+
})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
To skip logging for certain paths, use separate routers:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
// Health check without logging
|
|
298
|
+
const healthRouter = router({
|
|
299
|
+
get: async () => ({ status: 'ok' }),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// API routes with logging
|
|
303
|
+
const apiRouter = router({
|
|
304
|
+
users: router({ ... }),
|
|
305
|
+
}, logger());
|
|
306
|
+
|
|
307
|
+
// Combine
|
|
308
|
+
const app = router({
|
|
309
|
+
health: healthRouter,
|
|
310
|
+
api: apiRouter,
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
### rateLimit
|
|
317
|
+
|
|
318
|
+
Rate limiting with configurable store.
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
rateLimit({
|
|
322
|
+
// Time window in milliseconds (default: 60000 = 1 minute)
|
|
323
|
+
windowMs: 60 * 1000,
|
|
324
|
+
|
|
325
|
+
// Maximum requests per window (default: 100)
|
|
326
|
+
max: 100,
|
|
327
|
+
|
|
328
|
+
// Custom key generator (default: IP address from headers)
|
|
329
|
+
keyGenerator: (ctx) => {
|
|
330
|
+
return ctx.user?.id || getClientIp(ctx.request);
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
// Custom store (default: in-memory)
|
|
334
|
+
store: new RedisRateLimitStore(redis),
|
|
335
|
+
|
|
336
|
+
// Don't count successful requests (status < 400)
|
|
337
|
+
skipSuccessfulRequests: true,
|
|
338
|
+
|
|
339
|
+
// Don't count failed requests (status >= 400)
|
|
340
|
+
skipFailedRequests: true,
|
|
341
|
+
|
|
342
|
+
// Custom handler for rate-limited requests
|
|
343
|
+
handler: (ctx) => {
|
|
344
|
+
return new Response(JSON.stringify({ error: 'Slow down!' }), {
|
|
345
|
+
status: 429,
|
|
346
|
+
headers: { 'Content-Type': 'application/json' },
|
|
347
|
+
});
|
|
348
|
+
},
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Adds rate limit headers to responses:
|
|
353
|
+
- `X-RateLimit-Limit`: Maximum requests allowed
|
|
354
|
+
- `X-RateLimit-Remaining`: Remaining requests in window
|
|
355
|
+
- `X-RateLimit-Reset`: ISO timestamp when the window resets
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
### requestId
|
|
360
|
+
|
|
361
|
+
Adds a unique request ID to each request.
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
requestId({
|
|
365
|
+
// Header name (default: 'X-Request-ID')
|
|
366
|
+
headerName: 'X-Request-ID',
|
|
367
|
+
|
|
368
|
+
// Custom ID generator (default: crypto.randomUUID())
|
|
369
|
+
generator: () => `req-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
370
|
+
})
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Sets `ctx.requestId` and adds the ID to response headers.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
### timeout
|
|
378
|
+
|
|
379
|
+
Times out long-running requests.
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
timeout({
|
|
383
|
+
// Timeout in milliseconds (required)
|
|
384
|
+
timeout: 5000,
|
|
385
|
+
|
|
386
|
+
// Custom timeout message
|
|
387
|
+
message: 'Request took too long',
|
|
388
|
+
})
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Returns 408 Request Timeout on timeout.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
### contentType
|
|
396
|
+
|
|
397
|
+
Validates request Content-Type header.
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
contentType({
|
|
401
|
+
// Required: expected content types
|
|
402
|
+
types: ['application/json', 'application/x-www-form-urlencoded'],
|
|
403
|
+
|
|
404
|
+
// Skip validation for these methods (default: GET, HEAD, OPTIONS)
|
|
405
|
+
skipMethods: ['GET', 'HEAD', 'OPTIONS'],
|
|
406
|
+
|
|
407
|
+
// Custom error message
|
|
408
|
+
message: 'Only JSON is accepted',
|
|
409
|
+
})
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Returns 415 Unsupported Media Type on mismatch.
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## Middleware Utilities
|
|
417
|
+
|
|
418
|
+
### compose
|
|
419
|
+
|
|
420
|
+
Combines multiple middleware into a single middleware:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
import { compose } from '@fresho/router/middleware';
|
|
424
|
+
|
|
425
|
+
const security = compose(
|
|
426
|
+
cors(),
|
|
427
|
+
rateLimit({ max: 100 }),
|
|
428
|
+
errorHandler(),
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const api = router({
|
|
432
|
+
users: router({ ... }),
|
|
433
|
+
}, security);
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## See Also
|
|
439
|
+
|
|
440
|
+
- [Authentication Guide](authentication.md) - JWT, cookies, token refresh
|
|
441
|
+
- [Main Documentation](../README.md)
|
package/package.json
CHANGED
|
@@ -1,10 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fresho/router",
|
|
3
|
-
"
|
|
4
|
-
|
|
3
|
+
"engines": {
|
|
4
|
+
"node": ">=22"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.0.4",
|
|
7
|
+
"description": "Type-safe route definitions with auto-generated clients and OpenAPI docs",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"author": "fresho.dev",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/fresho-dev/router.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/fresho-dev/router/issues"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/fresho-dev/router#README.md",
|
|
5
18
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
19
|
+
"router",
|
|
20
|
+
"routing",
|
|
21
|
+
"typescript",
|
|
22
|
+
"type-safe",
|
|
23
|
+
"cloudflare-workers",
|
|
24
|
+
"deno",
|
|
25
|
+
"bun",
|
|
26
|
+
"http",
|
|
27
|
+
"api",
|
|
28
|
+
"rest",
|
|
29
|
+
"middleware",
|
|
30
|
+
"openapi",
|
|
31
|
+
"validation",
|
|
32
|
+
"schema"
|
|
33
|
+
],
|
|
34
|
+
"type": "module",
|
|
35
|
+
"sideEffects": false,
|
|
36
|
+
"main": "./dist/index.mjs",
|
|
37
|
+
"types": "./dist/index.d.mts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.mts",
|
|
41
|
+
"import": "./dist/index.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./middleware": {
|
|
44
|
+
"types": "./dist/middleware/index.d.mts",
|
|
45
|
+
"import": "./dist/middleware/index.mjs"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsdown src/index.ts src/middleware/index.ts",
|
|
50
|
+
"clean": "rm -rf ./dist",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"lint": "biome check . && npm run typecheck",
|
|
53
|
+
"lint:fix": "biome check --write .",
|
|
54
|
+
"test": "npm run lint && node --import tsx --test 'src/**/*.test.ts'"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@biomejs/biome": "2.3.8",
|
|
58
|
+
"@types/node": "^22.0.0",
|
|
59
|
+
"tsdown": "^0.17.0-beta.6",
|
|
60
|
+
"tsx": "^4.19.0",
|
|
61
|
+
"typescript": "^5.7.0"
|
|
62
|
+
},
|
|
63
|
+
"files": [
|
|
64
|
+
"dist",
|
|
65
|
+
"docs",
|
|
66
|
+
"LICENSE"
|
|
9
67
|
]
|
|
10
68
|
}
|