@bepalo/router 1.0.3
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 +557 -0
- package/dist/cjs/helpers.d.ts +290 -0
- package/dist/cjs/helpers.d.ts.map +1 -0
- package/dist/cjs/helpers.js +691 -0
- package/dist/cjs/helpers.js.map +1 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +21 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/list.d.ts +166 -0
- package/dist/cjs/list.d.ts.map +1 -0
- package/dist/cjs/list.js +483 -0
- package/dist/cjs/list.js.map +1 -0
- package/dist/cjs/middlewares.d.ts +251 -0
- package/dist/cjs/middlewares.d.ts.map +1 -0
- package/dist/cjs/middlewares.js +359 -0
- package/dist/cjs/middlewares.js.map +1 -0
- package/dist/cjs/router.d.ts +333 -0
- package/dist/cjs/router.d.ts.map +1 -0
- package/dist/cjs/router.js +659 -0
- package/dist/cjs/router.js.map +1 -0
- package/dist/cjs/tree.d.ts +18 -0
- package/dist/cjs/tree.d.ts.map +1 -0
- package/dist/cjs/tree.js +162 -0
- package/dist/cjs/tree.js.map +1 -0
- package/dist/cjs/types.d.ts +127 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/upload-stream.d.ts +105 -0
- package/dist/cjs/upload-stream.d.ts.map +1 -0
- package/dist/cjs/upload-stream.js +417 -0
- package/dist/cjs/upload-stream.js.map +1 -0
- package/dist/helpers.d.ts +290 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +691 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/list.d.ts +166 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/list.js +483 -0
- package/dist/list.js.map +1 -0
- package/dist/middlewares.d.ts +251 -0
- package/dist/middlewares.d.ts.map +1 -0
- package/dist/middlewares.js +359 -0
- package/dist/middlewares.js.map +1 -0
- package/dist/router.d.ts +333 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +659 -0
- package/dist/router.js.map +1 -0
- package/dist/tree.d.ts +18 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +162 -0
- package/dist/tree.js.map +1 -0
- package/dist/types.d.ts +127 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/upload-stream.d.ts +105 -0
- package/dist/upload-stream.d.ts.map +1 -0
- package/dist/upload-stream.js +417 -0
- package/dist/upload-stream.js.map +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 The Bepalo Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
# 🏆 @bepalo/router
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@bepalo/router)
|
|
4
|
+
[](https://github.com/bepalo/router/actions/workflows/ci.yaml)
|
|
5
|
+
[](https://github.com/bepalo/router/actions/workflows/testing.yaml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
[](test-result.md)
|
|
10
|
+
|
|
11
|
+
**A fast, feature-rich HTTP router for modern JavaScript runtimes.**
|
|
12
|
+
|
|
13
|
+
## ✨ Features
|
|
14
|
+
|
|
15
|
+
- ⚡ **High Performance** - Built on a radix tree for O(k) route matching (where k is path length)
|
|
16
|
+
- 🎯 **Flexible Routing** - Support for path parameters, wildcards (`*`, `.*`, `**`, `.**`), and all HTTP methods
|
|
17
|
+
- 🎭 **Multiple Handler Types** - Filters, hooks, handlers, fallbacks, and catchers
|
|
18
|
+
- 🔌 **Middleware Pipeline** - Chain multiple handlers with early exit capability
|
|
19
|
+
- 🛡️ **Error Handling** - Built-in error catching with contextual error handlers
|
|
20
|
+
- 🔄 **Method-Based Routing** - Separate routing trees for each HTTP method
|
|
21
|
+
- 📦 **Local Dependencies** - Only @bepalo dependencies
|
|
22
|
+
- 🌐 **Runtime Agnostic** - Works with Bun, Deno, Node.js, and other runtimes
|
|
23
|
+
- 🔧 **TypeScript Ready** - Full type definitions included
|
|
24
|
+
- 🧩 **Router Composition** - Append one router to another with a path prefix.
|
|
25
|
+
- 🛠️ **Helper Functions** - Built-in response helpers (json, html, parseBody, upload, etc.)
|
|
26
|
+
- 🔐 **Middleware Integration** - CORS, rate limiting, authentication helpers
|
|
27
|
+
|
|
28
|
+
## 🚀 Get Started
|
|
29
|
+
|
|
30
|
+
### 📥 Installation
|
|
31
|
+
|
|
32
|
+
**Node.js / Bun (npm / pnpm / yarn)**
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
bun add @bepalo/router
|
|
36
|
+
# or
|
|
37
|
+
pnpm add @bepalo/router
|
|
38
|
+
# or
|
|
39
|
+
npm install @bepalo/router
|
|
40
|
+
# or
|
|
41
|
+
yarn add @bepalo/router
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Deno**
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// Import directly using the URL:
|
|
48
|
+
|
|
49
|
+
import { Router } from "npm:@bepalo/router";
|
|
50
|
+
// or
|
|
51
|
+
import { Router } from "jsr:@bepalo/router";
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 📦 Basic Usage
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import {
|
|
58
|
+
Router,
|
|
59
|
+
text,
|
|
60
|
+
json,
|
|
61
|
+
CTXBody,
|
|
62
|
+
parseBody,
|
|
63
|
+
CTXUpload,
|
|
64
|
+
parseUploadStreaming,
|
|
65
|
+
} from "@bepalo/router";
|
|
66
|
+
|
|
67
|
+
// Create a router instance
|
|
68
|
+
const router = new Router();
|
|
69
|
+
|
|
70
|
+
// Simple GET route
|
|
71
|
+
router.handle("GET /", () => text("Hello World!"));
|
|
72
|
+
|
|
73
|
+
// Route with parameters
|
|
74
|
+
router.handle("GET /users/:id", (req, ctx) => json({ userId: ctx.params.id }));
|
|
75
|
+
|
|
76
|
+
// POST route with JSON response
|
|
77
|
+
router.handle("POST /users", async (req) => {
|
|
78
|
+
const body = await req.json();
|
|
79
|
+
return json({ created: true, data: body }, { status: 201 });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// 404 fallback
|
|
83
|
+
router.fallback("*", () => status(404));
|
|
84
|
+
|
|
85
|
+
// Error handler
|
|
86
|
+
router.catch("*", (req, ctx) => {
|
|
87
|
+
console.error("Error:", ctx.error);
|
|
88
|
+
return status(500, "Something Went Wrong");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Start server (Bun example)
|
|
92
|
+
Bun.serve({
|
|
93
|
+
port: 3000,
|
|
94
|
+
fetch: (req) => router.respond(req),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
console.log("Server running at http://localhost:3000");
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 📚 Core Concepts
|
|
101
|
+
|
|
102
|
+
### Handler Types & Execution Order
|
|
103
|
+
|
|
104
|
+
The router processes requests in this specific order:
|
|
105
|
+
|
|
106
|
+
1. Hooks (router.hook()) - Pre-processing, responses are ignored
|
|
107
|
+
|
|
108
|
+
2. Filters (router.filter()) - Request validation/authentication
|
|
109
|
+
|
|
110
|
+
3. Handlers (router.handle()) - Main request processing
|
|
111
|
+
|
|
112
|
+
4. Fallbacks (router.fallback()) - When no handler matches
|
|
113
|
+
|
|
114
|
+
5. Afters (router.after()) - Post-processing, responses are ignored
|
|
115
|
+
|
|
116
|
+
6. Catchers (router.catch()) - Error handling
|
|
117
|
+
|
|
118
|
+
### Router Context
|
|
119
|
+
|
|
120
|
+
Each handler receives a context object with:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
interface RouterContext {
|
|
124
|
+
params: Record<string, string>; // Route parameters
|
|
125
|
+
headers: Headers; // Response headers
|
|
126
|
+
address?: SocketAddress | null; // Client address
|
|
127
|
+
response?: Response; // Final response
|
|
128
|
+
error?: Error; // Caught error
|
|
129
|
+
hooksFound: boolean; // Whether hooks were found
|
|
130
|
+
aftersFound: boolean; // Whether afters were found
|
|
131
|
+
filtersFound: boolean; // Whether filters were found
|
|
132
|
+
handlersFound: boolean; // Whether handlers were found
|
|
133
|
+
fallbacksFound: boolean; // Whether fallbacks were found
|
|
134
|
+
catchersFound: boolean; // Whether catchers were found
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 📖 API Reference
|
|
139
|
+
|
|
140
|
+
### Router Class
|
|
141
|
+
|
|
142
|
+
#### Constructor
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
new Router<Context>(config?: RouterConfig<Context>)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### Configuration Options:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
interface RouterConfig<Context extends RouterContext> {
|
|
152
|
+
defaultHeaders?: Array<[string, string]>; // Default response headers
|
|
153
|
+
defaultCatcher?: Handler<Context>; // Global error handler
|
|
154
|
+
defaultFallback?: Handler<Context>; // Global fallback handler
|
|
155
|
+
enable?: HandlerEnable; // Handler types enable/disable
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Handler Registration Methods
|
|
160
|
+
|
|
161
|
+
All methods support method chaining and return the router instance. However `Response` type responses from hooks and afters are ignored unlike the rest.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// Register a hook (pre-processing)
|
|
165
|
+
router.hook(
|
|
166
|
+
urls: "*" | MethodPath | MethodPath[],
|
|
167
|
+
pipeline: Handler<Context> | Pipeline<Context>,
|
|
168
|
+
options?: HandlerOptions
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
// Register a filter (request validation)
|
|
172
|
+
router.filter(
|
|
173
|
+
urls: "*" | MethodPath | MethodPath[],
|
|
174
|
+
pipeline: Handler<Context> | Pipeline<Context>,
|
|
175
|
+
options?: HandlerOptions
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
// Register a main handler
|
|
179
|
+
router.handle(
|
|
180
|
+
urls: "*" | MethodPath | MethodPath[],
|
|
181
|
+
pipeline: Handler<Context> | Pipeline<Context>,
|
|
182
|
+
options?: HandlerOptions
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
// Register a fallback handler (404)
|
|
186
|
+
router.fallback(
|
|
187
|
+
urls: "*" | MethodPath | MethodPath[],
|
|
188
|
+
pipeline: Handler<Context> | Pipeline<Context>,
|
|
189
|
+
options?: HandlerOptions
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
// Register an error catcher
|
|
193
|
+
router.catch(
|
|
194
|
+
urls: "*" | MethodPath | MethodPath[],
|
|
195
|
+
pipeline: Handler<Context> | Pipeline<Context>,
|
|
196
|
+
options?: HandlerOptions
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
// Register an after handler (post-processing)
|
|
200
|
+
router.after(
|
|
201
|
+
urls: "*" | MethodPath | MethodPath[],
|
|
202
|
+
pipeline: Handler<Context> | Pipeline<Context>,
|
|
203
|
+
options?: HandlerOptions
|
|
204
|
+
)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### Handler Options:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
interface HandlerOptions {
|
|
211
|
+
overwrite?: boolean; // Allow overriding existing routes (default: false)
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### Router Composition
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// Append another router with a prefix
|
|
219
|
+
router.append(
|
|
220
|
+
baseUrl: `/${string}`,
|
|
221
|
+
router: Router<Context>,
|
|
222
|
+
options?: HandlerOptions
|
|
223
|
+
)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### Request Processing
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
// Process a request
|
|
230
|
+
router.respond(
|
|
231
|
+
req: Request,
|
|
232
|
+
context?: Partial<Context>
|
|
233
|
+
): Promise<Response>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### Helper Functions
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import {
|
|
240
|
+
status, // HTTP status response
|
|
241
|
+
text, // Plain text response
|
|
242
|
+
html, // HTML response
|
|
243
|
+
json, // JSON response
|
|
244
|
+
blob, // Blob response
|
|
245
|
+
octetStream, // Octet-stream response
|
|
246
|
+
formData, // FormData response
|
|
247
|
+
usp, // URLSearchParams response
|
|
248
|
+
send, // Smart response (auto-detects content type)
|
|
249
|
+
setCookie, // Set cookie header
|
|
250
|
+
clearCookie, // Clear cookie
|
|
251
|
+
CTXCookie,
|
|
252
|
+
parseCookie, // Cookie parser
|
|
253
|
+
CTXBody,
|
|
254
|
+
parseBody, // Body parser
|
|
255
|
+
CTXUpload,
|
|
256
|
+
parseUploadStreaming, // multi-part-form-data and file upload stream parser
|
|
257
|
+
} from "@bepalo/router";
|
|
258
|
+
|
|
259
|
+
// Usage examples
|
|
260
|
+
router.handle("GET /text", () => text("Hello World"));
|
|
261
|
+
router.handle("GET /html", () => html("<h1>Title</h1>"));
|
|
262
|
+
router.handle("GET /json", () => json({ data: "value" }));
|
|
263
|
+
router.handle("GET /status", () => status(204, null)); // No Content
|
|
264
|
+
router.handle("GET /intro.mp4", () => blob(Bun.file("./intro.mp4")));
|
|
265
|
+
router.handle("GET /download", () => octetStream(Bun.file("./intro.mp4")));
|
|
266
|
+
router.handle<CTXCookie>("GET /cookie", [
|
|
267
|
+
parseCookie(),
|
|
268
|
+
(req, { cookie }) => json({ cookie }),
|
|
269
|
+
]);
|
|
270
|
+
router.handle<CTXBody>("POST /cookie", [
|
|
271
|
+
parseBody(),
|
|
272
|
+
(req, { body }) => {
|
|
273
|
+
return status(200, "OK", {
|
|
274
|
+
headers: [
|
|
275
|
+
...Object.entries(body).map(([name, value]) =>
|
|
276
|
+
setCookie(name, String(value), {
|
|
277
|
+
path: "/",
|
|
278
|
+
expires: Time.after(5).minutes.fromNow()._ms,
|
|
279
|
+
}),
|
|
280
|
+
),
|
|
281
|
+
],
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
]);
|
|
285
|
+
router.handle("DELETE /cookie/:name", [
|
|
286
|
+
(req, ctx) =>
|
|
287
|
+
status(200, "OK", {
|
|
288
|
+
headers: [clearCookie(ctx.params.name, { path: "/" })],
|
|
289
|
+
}),
|
|
290
|
+
]);
|
|
291
|
+
router.handle<CTXUpload>("POST /upload", [
|
|
292
|
+
(req, ctx) => {
|
|
293
|
+
let file: Bun.BunFile;
|
|
294
|
+
let fileWriter: Bun.FileSink;
|
|
295
|
+
return parseUploadStreaming({
|
|
296
|
+
allowedTypes: ["image/jpeg"],
|
|
297
|
+
async onFileStart(uploadId, fieldName, fileName, contentType, fileSize) {
|
|
298
|
+
console.log(fileSize);
|
|
299
|
+
const ext = fileName.substring(fileName.lastIndexOf("."));
|
|
300
|
+
file = Bun.file("./uploads/" + uploadId + ext);
|
|
301
|
+
fileWriter = file.writer();
|
|
302
|
+
},
|
|
303
|
+
async onFileChunk(uploadId, fieldName, fileName, chunk, offset, isLast) {
|
|
304
|
+
fileWriter.write(chunk);
|
|
305
|
+
},
|
|
306
|
+
async onFileComplete(
|
|
307
|
+
uploadId,
|
|
308
|
+
fieldName,
|
|
309
|
+
fileName,
|
|
310
|
+
fileSize,
|
|
311
|
+
customFilename,
|
|
312
|
+
metadata,
|
|
313
|
+
) {
|
|
314
|
+
console.log({ uploadId, fieldName, fileName, fileSize });
|
|
315
|
+
},
|
|
316
|
+
async onUploadComplete(uploadId, success) {
|
|
317
|
+
console.log({ uploadId, success });
|
|
318
|
+
},
|
|
319
|
+
})(req, ctx);
|
|
320
|
+
},
|
|
321
|
+
(req, { uploadId, fields, files }) => {
|
|
322
|
+
console.log({ uploadId, fields, files });
|
|
323
|
+
console.log(files.get("profile"));
|
|
324
|
+
return status(200);
|
|
325
|
+
},
|
|
326
|
+
]);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
#### Provided Middleware
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
import {
|
|
333
|
+
cors, // CORS middleware
|
|
334
|
+
limitRate, // Rate limiting
|
|
335
|
+
authBasic, // Basic authentication
|
|
336
|
+
authAPIKey, // API key authentication
|
|
337
|
+
authJWT, // JWT authentication
|
|
338
|
+
} from "@bepalo/router/middlewares";
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### 🔧 Advanced Usage
|
|
342
|
+
|
|
343
|
+
#### Pipeline (Multiple Handlers)
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
router.handle("POST /api/users", [
|
|
347
|
+
// Middleware 1: Parse body
|
|
348
|
+
async (req, ctx) => {
|
|
349
|
+
const body = await req.json();
|
|
350
|
+
ctx.body = body;
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
// Middleware 2: Validate
|
|
354
|
+
(req, ctx) => {
|
|
355
|
+
if (!ctx.body.email) {
|
|
356
|
+
return text("Email is required", { status: 400 });
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
// Handler: Process request
|
|
361
|
+
async (req, ctx) => {
|
|
362
|
+
const user = await db.users.create(ctx.body);
|
|
363
|
+
return json(user, { status: 201 });
|
|
364
|
+
},
|
|
365
|
+
]);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Path Patterns
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
// Named parameters
|
|
372
|
+
router.handle("GET /users/:id", handler); // Matches: /users/123
|
|
373
|
+
|
|
374
|
+
// Single segment wildcard
|
|
375
|
+
router.handle("GET /files/*", handler); // Matches: /files/a, /files/b
|
|
376
|
+
|
|
377
|
+
// Single segment wildcard including current path (must be at end)
|
|
378
|
+
router.handle("GET /files/.*", handler); // Matches: /files, /files/a, /files/b
|
|
379
|
+
|
|
380
|
+
// Multi-segment wildcard (must be at end)
|
|
381
|
+
router.handle("GET /docs/**", handler); // Matches: /docs/a, /docs/a/b/c
|
|
382
|
+
|
|
383
|
+
// Multi-segment wildcard including current path (must be at end)
|
|
384
|
+
router.handle("GET /docs/.**", handler); // Matches: /docs, /docs/a, /docs/a/b/c
|
|
385
|
+
|
|
386
|
+
// Mixed patterns
|
|
387
|
+
router.handle("GET /api/:version/*/details", handler); // /api/v1/users/details
|
|
388
|
+
|
|
389
|
+
// All method-paths
|
|
390
|
+
router.handle("*", handler); // GET/POST/PUT/etc /.**
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Route Priority
|
|
394
|
+
|
|
395
|
+
Routes are matched in this order of priority:
|
|
396
|
+
|
|
397
|
+
1. Exact path matches
|
|
398
|
+
|
|
399
|
+
2. Path parameters (:id) and Single segment wildcards (*, .*)
|
|
400
|
+
|
|
401
|
+
3. Multi-segment wildcards (**, .**)
|
|
402
|
+
|
|
403
|
+
### Router Composition Example
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
const apiRouter = new Router();
|
|
407
|
+
|
|
408
|
+
// API routes
|
|
409
|
+
apiRouter.handle("GET /users", () => json({ users: [] }));
|
|
410
|
+
apiRouter.handle("POST /users", async (req) => {
|
|
411
|
+
const body = await req.json();
|
|
412
|
+
return json({ created: true, data: body }, { status: 201 });
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Nested router
|
|
416
|
+
const v1Router = new Router();
|
|
417
|
+
v1Router.handle("GET /status", () => json({ version: "1.0", status: "ok" }));
|
|
418
|
+
apiRouter.append("/v1", v1Router);
|
|
419
|
+
|
|
420
|
+
// Mount API router under /api
|
|
421
|
+
const mainRouter = new Router();
|
|
422
|
+
mainRouter.append("/api", apiRouter);
|
|
423
|
+
|
|
424
|
+
// Add some frontend routes
|
|
425
|
+
mainRouter.handle("GET /", () => html("<h1>Home</h1>"));
|
|
426
|
+
mainRouter.handle("GET /about", () => html("<h1>About</h1>"));
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Complete Example with Middleware
|
|
430
|
+
|
|
431
|
+
```js
|
|
432
|
+
import {
|
|
433
|
+
Router,
|
|
434
|
+
text,
|
|
435
|
+
json,
|
|
436
|
+
cors,
|
|
437
|
+
limitRate,
|
|
438
|
+
CTXAddress,
|
|
439
|
+
SocketAddress,
|
|
440
|
+
RouterContext,
|
|
441
|
+
} from "@bepalo/router";
|
|
442
|
+
|
|
443
|
+
const router = new Router<RouterContext & CTXAddress>({
|
|
444
|
+
defaultHeaders: [["X-Powered-By", "@bepalo/router"]],
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Global CORS
|
|
448
|
+
router.filter("*", [
|
|
449
|
+
cors({
|
|
450
|
+
origins: ["http://localhost:3000", "https://example.com"],
|
|
451
|
+
methods: ["GET", "POST", "PUT", "DELETE"],
|
|
452
|
+
credentials: true,
|
|
453
|
+
}),
|
|
454
|
+
]);
|
|
455
|
+
|
|
456
|
+
// Rate limiting for API
|
|
457
|
+
router.filter("GET /api/.**", [
|
|
458
|
+
limitRate({
|
|
459
|
+
key: (req, ctx) => ctx.address.address || "unknown",
|
|
460
|
+
maxTokens: 100,
|
|
461
|
+
refillRate: 10, // 10 tokens per second
|
|
462
|
+
setXRateLimitHeaders: true,
|
|
463
|
+
}),
|
|
464
|
+
]);
|
|
465
|
+
|
|
466
|
+
// Routes
|
|
467
|
+
router.handle("GET /", () => text("Welcome to the API"));
|
|
468
|
+
|
|
469
|
+
// users API `/api/users` router
|
|
470
|
+
{
|
|
471
|
+
const usersAPI = new Router();
|
|
472
|
+
|
|
473
|
+
usersAPI.handle("POST /", async (req) => {
|
|
474
|
+
const body = await req.json();
|
|
475
|
+
return json({ id: Date.now(), ...body }, { status: 201 });
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
usersAPI.handle("GET /", () =>
|
|
479
|
+
json({
|
|
480
|
+
users: [
|
|
481
|
+
{ id: 1, name: "Abebe" },
|
|
482
|
+
{ id: 2, name: "Derartu" },
|
|
483
|
+
],
|
|
484
|
+
}),
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
usersAPI.handle("GET /:id", (req, { params }) =>
|
|
488
|
+
json({ user: { id: params.id, name: "User " + params.id } }),
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
router.append("/api/users", usersAPI);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Custom Error handling
|
|
495
|
+
router.catch("*", (req, ctx) => {
|
|
496
|
+
console.error("Error:", ctx.error);
|
|
497
|
+
return json({ error: "Internal server error" }, { status: 500 });
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Custom fallback handler
|
|
501
|
+
router.fallback("*", () => json({ error: "Not found" }, { status: 404 }));
|
|
502
|
+
|
|
503
|
+
// Start server
|
|
504
|
+
Bun.serve({
|
|
505
|
+
port: 3000,
|
|
506
|
+
async fetch(req, server) {
|
|
507
|
+
const address = server.requestIP(req) as SocketAddress;
|
|
508
|
+
return await router.respond(req, { address });
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
console.log("Server listening on http://localhost:3000");
|
|
513
|
+
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## 🎯 Performance
|
|
517
|
+
|
|
518
|
+
The router uses a radix tree (trie) data structure for route matching, providing:
|
|
519
|
+
|
|
520
|
+
O(k) lookup time where k is the path length
|
|
521
|
+
|
|
522
|
+
Minimal memory usage - shared prefixes are stored only once
|
|
523
|
+
|
|
524
|
+
Fast parameter extraction - no regex matching overhead
|
|
525
|
+
|
|
526
|
+
Efficient wildcard matching - optimized tree traversal
|
|
527
|
+
|
|
528
|
+
### 📋 Comparison with Other Routers
|
|
529
|
+
|
|
530
|
+
| Feature | @bepalo/router | Express | Hono | Fastify |
|
|
531
|
+
| ------------------------------- | -------------- | ------- | ---- | ------- |
|
|
532
|
+
| Radix Tree Routing | ✅ | ❌ | ✅ | ✅ |
|
|
533
|
+
| Zero Dependencies | ✅ | ❌ | ❌ | ❌ |
|
|
534
|
+
| TypeScript Native | ✅ | ❌ | ✅ | ✅ |
|
|
535
|
+
| Extended Handler Phases | ✅ | ⚠️ | ⚠️ | ⚠️ |
|
|
536
|
+
| Built-in Middleware | ✅ | ❌ | ✅ | ✅ |
|
|
537
|
+
| Runtime Agnostic | ✅ | ❌ | ✅ | ❌ |
|
|
538
|
+
| Router Composition | ✅ | ✅ | ✅ | ✅ |
|
|
539
|
+
| Structured Multi-Phase Pipeline | ✅ | ❌ | ❌ | ❌ |
|
|
540
|
+
|
|
541
|
+
## 📄 License
|
|
542
|
+
|
|
543
|
+
This project is licensed under the MIT License - see the [LICENSE](/LICENSE) file for details.
|
|
544
|
+
|
|
545
|
+
## 🕊️ Thanks and Enjoy
|
|
546
|
+
|
|
547
|
+
If you like this library and want to support then please give a star on [GitHub](https://github.com/bepalo/router).
|
|
548
|
+
|
|
549
|
+
## 💖 Be a Sponsor
|
|
550
|
+
|
|
551
|
+
Fund me so I can give more attention to the products and services you liked.
|
|
552
|
+
|
|
553
|
+
<p align="left">
|
|
554
|
+
<a href="https://ko-fi.com/natieshzed" target="_blank">
|
|
555
|
+
<img height="32" src="https://img.shields.io/badge/Ko--fi-donate-orange?style=for-the-badge&logo=ko-fi&logoColor=white" alt="Ko-fi Badge">
|
|
556
|
+
</a>
|
|
557
|
+
</p>
|