@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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +557 -0
  3. package/dist/cjs/helpers.d.ts +290 -0
  4. package/dist/cjs/helpers.d.ts.map +1 -0
  5. package/dist/cjs/helpers.js +691 -0
  6. package/dist/cjs/helpers.js.map +1 -0
  7. package/dist/cjs/index.d.ts +5 -0
  8. package/dist/cjs/index.d.ts.map +1 -0
  9. package/dist/cjs/index.js +21 -0
  10. package/dist/cjs/index.js.map +1 -0
  11. package/dist/cjs/list.d.ts +166 -0
  12. package/dist/cjs/list.d.ts.map +1 -0
  13. package/dist/cjs/list.js +483 -0
  14. package/dist/cjs/list.js.map +1 -0
  15. package/dist/cjs/middlewares.d.ts +251 -0
  16. package/dist/cjs/middlewares.d.ts.map +1 -0
  17. package/dist/cjs/middlewares.js +359 -0
  18. package/dist/cjs/middlewares.js.map +1 -0
  19. package/dist/cjs/router.d.ts +333 -0
  20. package/dist/cjs/router.d.ts.map +1 -0
  21. package/dist/cjs/router.js +659 -0
  22. package/dist/cjs/router.js.map +1 -0
  23. package/dist/cjs/tree.d.ts +18 -0
  24. package/dist/cjs/tree.d.ts.map +1 -0
  25. package/dist/cjs/tree.js +162 -0
  26. package/dist/cjs/tree.js.map +1 -0
  27. package/dist/cjs/types.d.ts +127 -0
  28. package/dist/cjs/types.d.ts.map +1 -0
  29. package/dist/cjs/types.js +3 -0
  30. package/dist/cjs/types.js.map +1 -0
  31. package/dist/cjs/upload-stream.d.ts +105 -0
  32. package/dist/cjs/upload-stream.d.ts.map +1 -0
  33. package/dist/cjs/upload-stream.js +417 -0
  34. package/dist/cjs/upload-stream.js.map +1 -0
  35. package/dist/helpers.d.ts +290 -0
  36. package/dist/helpers.d.ts.map +1 -0
  37. package/dist/helpers.js +691 -0
  38. package/dist/helpers.js.map +1 -0
  39. package/dist/index.d.ts +5 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +21 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/list.d.ts +166 -0
  44. package/dist/list.d.ts.map +1 -0
  45. package/dist/list.js +483 -0
  46. package/dist/list.js.map +1 -0
  47. package/dist/middlewares.d.ts +251 -0
  48. package/dist/middlewares.d.ts.map +1 -0
  49. package/dist/middlewares.js +359 -0
  50. package/dist/middlewares.js.map +1 -0
  51. package/dist/router.d.ts +333 -0
  52. package/dist/router.d.ts.map +1 -0
  53. package/dist/router.js +659 -0
  54. package/dist/router.js.map +1 -0
  55. package/dist/tree.d.ts +18 -0
  56. package/dist/tree.d.ts.map +1 -0
  57. package/dist/tree.js +162 -0
  58. package/dist/tree.js.map +1 -0
  59. package/dist/types.d.ts +127 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +3 -0
  62. package/dist/types.js.map +1 -0
  63. package/dist/upload-stream.d.ts +105 -0
  64. package/dist/upload-stream.d.ts.map +1 -0
  65. package/dist/upload-stream.js +417 -0
  66. package/dist/upload-stream.js.map +1 -0
  67. 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
+ [![npm version](https://img.shields.io/npm/v/@bepalo/router.svg)](https://www.npmjs.com/package/@bepalo/router)
4
+ [![CI](https://img.shields.io/github/actions/workflow/status/bepalo/router/ci.yaml?label=ci)](https://github.com/bepalo/router/actions/workflows/ci.yaml)
5
+ [![tests](https://img.shields.io/github/actions/workflow/status/bepalo/router/testing.yaml?label=tests)](https://github.com/bepalo/router/actions/workflows/testing.yaml)
6
+ [![license](https://img.shields.io/npm/l/@bepalo/router.svg)](LICENSE)
7
+ ![Benchmarked](https://img.shields.io/badge/benchmarked-yes-green)
8
+
9
+ [![Vitest](https://img.shields.io/badge/vitest-6E9F18?style=for-the-badge&logo=vitest&logoColor=white)](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>