@nextrush/router 3.0.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tanzim (NextRush)
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,618 @@
1
+ # @nextrush/router
2
+
3
+ > High-performance radix tree router for NextRush. O(k) route matching where k = path length, not route count.
4
+
5
+ ## The Problem
6
+
7
+ Traditional array-based routers iterate through all routes to find a match. With 1,000 routes, that's 1,000 comparisons per request. Route order matters. Performance degrades linearly.
8
+
9
+ ## How NextRush Approaches This
10
+
11
+ The router uses a **radix tree** (compressed prefix tree):
12
+
13
+ - Routes share common prefixes in the tree structure
14
+ - Matching is O(k) where k is path length (typically 10-50 characters)
15
+ - Route count doesn't affect matching speed
16
+ - Memory efficient: shared prefixes stored once
17
+
18
+ ## Mental Model
19
+
20
+ ```
21
+ Routes:
22
+ /users
23
+ /users/:id
24
+ /users/:id/posts
25
+ /products
26
+ /products/:id
27
+
28
+ Tree:
29
+ /
30
+ ├── users
31
+ │ └── /:id
32
+ │ └── /posts
33
+ └── products
34
+ └── /:id
35
+ ```
36
+
37
+ When matching `/users/123/posts`:
38
+
39
+ 1. Match `/` → found
40
+ 2. Match `users` → found
41
+ 3. Match `/:id` → captures `123`
42
+ 4. Match `/posts` → found
43
+ 5. Return handler + params
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pnpm add @nextrush/router
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ```typescript
54
+ import { createApp } from '@nextrush/core';
55
+ import { createRouter } from '@nextrush/router';
56
+
57
+ const app = createApp();
58
+ const router = createRouter();
59
+
60
+ router.get('/', (ctx) => {
61
+ ctx.json({ message: 'Home' });
62
+ });
63
+
64
+ router.get('/users/:id', (ctx) => {
65
+ ctx.json({ userId: ctx.params.id });
66
+ });
67
+
68
+ app.route('/', router);
69
+ app.listen(3000);
70
+ ```
71
+
72
+ ## Features
73
+
74
+ - **Radix Tree Algorithm**: O(k) lookup where k = path length
75
+ - **Route Parameters**: Named parameters with `:param` syntax
76
+ - **Wildcard Routes**: Catch-all with `*` parameter
77
+ - **Route Groups**: Prefix-based grouping with middleware
78
+ - **Method-Based Routing**: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
79
+ - **Redirects**: Built-in redirect support with parameter interpolation
80
+ - **Zero Dependencies**: Only depends on `@nextrush/types`
81
+
82
+ ## Route Patterns
83
+
84
+ ### Static Routes
85
+
86
+ ```typescript
87
+ router.get('/api/users', handler);
88
+ router.get('/api/posts', handler);
89
+ router.get('/health', handler);
90
+ ```
91
+
92
+ ### Named Parameters
93
+
94
+ ```typescript
95
+ // Single parameter
96
+ router.get('/users/:id', (ctx) => {
97
+ ctx.json({ id: ctx.params.id });
98
+ });
99
+
100
+ // Multiple parameters
101
+ router.get('/users/:userId/posts/:postId', (ctx) => {
102
+ const { userId, postId } = ctx.params;
103
+ ctx.json({ userId, postId });
104
+ });
105
+ ```
106
+
107
+ ### Wildcard Routes
108
+
109
+ ```typescript
110
+ // Catch-all: captures everything after /files/
111
+ router.get('/files/*', (ctx) => {
112
+ const filePath = ctx.params['*'];
113
+ ctx.json({ path: filePath });
114
+ });
115
+
116
+ // /files/docs/readme.md → ctx.params['*'] = 'docs/readme.md'
117
+ ```
118
+
119
+ Wildcards must be at the end of the route. Everything after the `*` segment is captured.
120
+
121
+ ## HTTP Methods
122
+
123
+ ```typescript
124
+ router.get('/resource', handler);
125
+ router.post('/resource', handler);
126
+ router.put('/resource/:id', handler);
127
+ router.patch('/resource/:id', handler);
128
+ router.delete('/resource/:id', handler);
129
+ router.head('/resource', handler);
130
+ router.options('/resource', handler);
131
+
132
+ // Register for all standard methods (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
133
+ // TRACE and CONNECT are excluded for security reasons
134
+ router.all('/any', handler);
135
+
136
+ // Register specific method dynamically
137
+ router.route('GET', '/dynamic', handler);
138
+ ```
139
+
140
+ ## Route Groups
141
+
142
+ Group routes with a common prefix:
143
+
144
+ ```typescript
145
+ const api = createRouter({ prefix: '/api/v1' });
146
+
147
+ api.get('/users', listUsers); // GET /api/v1/users
148
+ api.get('/users/:id', getUser); // GET /api/v1/users/:id
149
+ api.post('/users', createUser); // POST /api/v1/users
150
+
151
+ app.route('/', api);
152
+ ```
153
+
154
+ ### Nested Groups
155
+
156
+ ```typescript
157
+ const router = createRouter();
158
+
159
+ router.group('/api', (api) => {
160
+ api.group('/v1', (v1) => {
161
+ v1.get('/users', handler); // GET /api/v1/users
162
+ v1.get('/posts', handler); // GET /api/v1/posts
163
+ });
164
+
165
+ api.group('/v2', (v2) => {
166
+ v2.get('/users', handler); // GET /api/v2/users
167
+ });
168
+ });
169
+
170
+ app.route('/', router);
171
+ ```
172
+
173
+ ### Group Middleware
174
+
175
+ Apply middleware to all routes in a group:
176
+
177
+ ```typescript
178
+ const auth = async (ctx, next) => {
179
+ if (!ctx.get('Authorization')) {
180
+ ctx.status = 401;
181
+ ctx.json({ error: 'Unauthorized' });
182
+ return;
183
+ }
184
+ await next();
185
+ };
186
+
187
+ router.group('/admin', [auth], (admin) => {
188
+ admin.get('/dashboard', getDashboard);
189
+ admin.get('/settings', getSettings);
190
+ });
191
+ ```
192
+
193
+ ## Route Middleware
194
+
195
+ Apply middleware to specific routes:
196
+
197
+ ```typescript
198
+ const auth = async (ctx, next) => {
199
+ if (!ctx.get('Authorization')) {
200
+ ctx.status = 401;
201
+ ctx.json({ error: 'Unauthorized' });
202
+ return;
203
+ }
204
+ await next();
205
+ };
206
+
207
+ // Single route with middleware
208
+ router.get('/protected', auth, handler);
209
+
210
+ // Multiple middleware
211
+ router.get('/admin', auth, adminOnly, handler);
212
+ ```
213
+
214
+ Middleware executes in order: `auth` → `adminOnly` → `handler`
215
+
216
+ ## Redirects
217
+
218
+ Built-in support for HTTP redirects:
219
+
220
+ ```typescript
221
+ // Permanent redirect (301)
222
+ router.redirect('/old-page', '/new-page');
223
+
224
+ // Temporary redirect (302)
225
+ router.redirect('/temp', '/destination', 302);
226
+
227
+ // Redirect to external URL
228
+ router.redirect('/docs', 'https://docs.example.com');
229
+
230
+ // With parameter interpolation
231
+ router.redirect('/users/:id', '/profiles/:id');
232
+ // /users/123 → redirects to /profiles/123
233
+ ```
234
+
235
+ Supported status codes: `301`, `302`, `303`, `307`, `308`
236
+
237
+ ## Sub-Router Mounting
238
+
239
+ Compose routers together using `mount()` or `use()`:
240
+
241
+ ### Using mount() (Recommended)
242
+
243
+ ```typescript
244
+ const userRouter = createRouter();
245
+ userRouter.get('/', listUsers);
246
+ userRouter.get('/:id', getUser);
247
+ userRouter.post('/', createUser);
248
+
249
+ const postRouter = createRouter();
250
+ postRouter.get('/', listPosts);
251
+ postRouter.get('/:id', getPost);
252
+
253
+ const api = createRouter();
254
+ api.mount('/users', userRouter); // Clean explicit mounting
255
+ api.mount('/posts', postRouter);
256
+
257
+ app.route('/api', api);
258
+ // Results in: /api/users, /api/users/:id, /api/posts, /api/posts/:id
259
+ ```
260
+
261
+ ### Using use() (Classic Pattern)
262
+
263
+ The traditional `use()` method also works:
264
+
265
+ ```typescript
266
+ const api = createRouter();
267
+ api.use('/users', userRouter);
268
+ api.use('/posts', postRouter);
269
+
270
+ app.route('/api', api);
271
+ // Or classic: app.use(api.routes());
272
+ ```
273
+
274
+ ### Direct App Mounting (Hono-Style)
275
+
276
+ For the cleanest DX, mount routers directly on the app:
277
+
278
+ ```typescript
279
+ import { createApp } from '@nextrush/core';
280
+
281
+ const app = createApp();
282
+ app.route('/users', userRouter); // No routes() call needed!
283
+ app.route('/posts', postRouter);
284
+ ```
285
+
286
+ ## Allowed Methods
287
+
288
+ Automatically handle 405 Method Not Allowed:
289
+
290
+ ```typescript
291
+ app.route('/', router);
292
+ app.use(router.allowedMethods());
293
+
294
+ // GET /users → 200 OK (if GET handler exists)
295
+ // POST /users (no POST handler) → 405 Method Not Allowed
296
+ // OPTIONS /users → Returns allowed methods in Allow header
297
+ ```
298
+
299
+ ## Router Options
300
+
301
+ ```typescript
302
+ interface RouterOptions {
303
+ // Route prefix prepended to all routes
304
+ prefix?: string;
305
+
306
+ // Case sensitive matching (default: false)
307
+ caseSensitive?: boolean;
308
+
309
+ // Strict trailing slash handling (default: false)
310
+ strict?: boolean;
311
+ }
312
+
313
+ const router = createRouter({
314
+ prefix: '/api/v1',
315
+ caseSensitive: false,
316
+ strict: false,
317
+ });
318
+ ```
319
+
320
+ ### Case Sensitivity
321
+
322
+ ```typescript
323
+ // Default: case-insensitive
324
+ const router = createRouter();
325
+ router.get('/Users', handler);
326
+ router.match('GET', '/users'); // ✓ matches
327
+
328
+ // Case-sensitive mode
329
+ const router = createRouter({ caseSensitive: true });
330
+ router.get('/Users', handler);
331
+ router.match('GET', '/Users'); // ✓ matches
332
+ router.match('GET', '/users'); // ✗ no match
333
+ ```
334
+
335
+ ### Trailing Slash
336
+
337
+ ```typescript
338
+ // Default: trailing slashes are normalized
339
+ router.get('/users', handler);
340
+ router.match('GET', '/users'); // ✓ matches
341
+ router.match('GET', '/users/'); // ✓ matches (normalized)
342
+ ```
343
+
344
+ ## Performance
345
+
346
+ The radix tree router provides:
347
+
348
+ - **O(k) Lookup**: Where k is the path length, not route count
349
+ - **Memory Efficient**: Shared prefixes are stored once
350
+ - **Fast Parameter Extraction**: Single-pass parsing
351
+
352
+ ### Benchmarks
353
+
354
+ | Routes | Lookup Time |
355
+ | ------ | ----------- |
356
+ | 100 | ~0.02ms |
357
+ | 1,000 | ~0.02ms |
358
+ | 10,000 | ~0.02ms |
359
+
360
+ Route count doesn't affect lookup time significantly.
361
+
362
+ ## API Reference
363
+
364
+ ### `createRouter(options?)`
365
+
366
+ Create a new router instance.
367
+
368
+ ```typescript
369
+ function createRouter(options?: RouterOptions): Router;
370
+ ```
371
+
372
+ **Parameters:**
373
+
374
+ | Parameter | Type | Required | Default | Description |
375
+ | --------- | --------------- | -------- | ------- | -------------------- |
376
+ | `options` | `RouterOptions` | No | `{}` | Router configuration |
377
+
378
+ **Options:**
379
+
380
+ | Option | Type | Default | Description |
381
+ | --------------- | --------- | ------- | ------------------------------------- |
382
+ | `prefix` | `string` | `''` | Path prefix for all routes |
383
+ | `caseSensitive` | `boolean` | `false` | Enable case-sensitive matching |
384
+ | `strict` | `boolean` | `false` | Enable strict trailing slash handling |
385
+
386
+ **Returns:** `Router` instance
387
+
388
+ ### Router Methods
389
+
390
+ #### HTTP Method Shortcuts
391
+
392
+ ```typescript
393
+ router.get(path: string, ...handlers: RouteHandler[]): this
394
+ router.post(path: string, ...handlers: RouteHandler[]): this
395
+ router.put(path: string, ...handlers: RouteHandler[]): this
396
+ router.delete(path: string, ...handlers: RouteHandler[]): this
397
+ router.patch(path: string, ...handlers: RouteHandler[]): this
398
+ router.head(path: string, ...handlers: RouteHandler[]): this
399
+ router.options(path: string, ...handlers: RouteHandler[]): this
400
+ router.all(path: string, ...handlers: RouteHandler[]): this
401
+ router.route(method: HttpMethod, path: string, ...handlers: RouteHandler[]): this
402
+ ```
403
+
404
+ #### `router.redirect(from, to, status?)`
405
+
406
+ Register a redirect route.
407
+
408
+ ```typescript
409
+ router.redirect(from: string, to: string, status?: 301 | 302 | 303 | 307 | 308): this
410
+ ```
411
+
412
+ Default status is `301` (Moved Permanently). Status codes `307` and `308` register handlers for all standard HTTP methods (to preserve the original method). Other codes register GET and HEAD only.
413
+
414
+ #### `router.group(prefix, middleware?, callback)`
415
+
416
+ Create a route group with shared prefix and middleware.
417
+
418
+ ```typescript
419
+ router.group(prefix: string, callback: (router: Router) => void): this
420
+ router.group(prefix: string, middleware: Middleware[], callback: (router: Router) => void): this
421
+ ```
422
+
423
+ #### `router.use(pathOrMiddleware, routerOrUndefined?)`
424
+
425
+ Mount middleware or sub-router.
426
+
427
+ ```typescript
428
+ router.use(middleware: Middleware): this
429
+ router.use(path: string, subRouter: Router): this
430
+ router.use(subRouter: Router): this
431
+ ```
432
+
433
+ #### `router.mount(path, subRouter)`
434
+
435
+ Mount a sub-router at a path prefix. Equivalent to `use(path, router)` with clearer intent.
436
+
437
+ ```typescript
438
+ router.mount(path: string, subRouter: Router): this
439
+ ```
440
+
441
+ **Example:**
442
+
443
+ ```typescript
444
+ const api = createRouter();
445
+ api.mount('/users', userRouter);
446
+ api.mount('/posts', postRouter);
447
+ ```
448
+
449
+ #### `router.match(method, path)`
450
+
451
+ Match a route and return handler + params.
452
+
453
+ ```typescript
454
+ router.match(method: HttpMethod, path: string): RouteMatch | null
455
+ ```
456
+
457
+ **Returns:**
458
+
459
+ ```typescript
460
+ interface RouteMatch {
461
+ handler: RouteHandler;
462
+ params: Record<string, string>;
463
+ middleware: Middleware[];
464
+ }
465
+ ```
466
+
467
+ #### `router.reset()`
468
+
469
+ Clear all registered routes, middleware, and internal state. Useful for testing or plugin teardown.
470
+
471
+ ```typescript
472
+ const router = createRouter();
473
+ router.get('/users', handler);
474
+
475
+ router.reset();
476
+ // All routes, static route cache, middleware, param/wildcard children are cleared
477
+ ```
478
+
479
+ > Calling `reset()` makes the router reusable — you can register fresh routes after resetting.
480
+
481
+ #### `router.routes()`
482
+
483
+ Get middleware function to mount on application.
484
+
485
+ ```typescript
486
+ const middleware = router.routes();
487
+ app.use(middleware);
488
+ ```
489
+
490
+ #### `router.allowedMethods()`
491
+
492
+ Get middleware that handles 405 responses and OPTIONS requests.
493
+
494
+ ```typescript
495
+ app.use(router.allowedMethods());
496
+ ```
497
+
498
+ ## TypeScript Types
499
+
500
+ ```typescript
501
+ import { createRouter, Router } from '@nextrush/router';
502
+
503
+ import type {
504
+ HttpMethod,
505
+ Middleware,
506
+ Route,
507
+ RouteHandler,
508
+ RouteMatch,
509
+ Router as RouterInterface,
510
+ RouterOptions,
511
+ } from '@nextrush/router';
512
+ ```
513
+
514
+ ## Common Patterns
515
+
516
+ ### RESTful Resource Routes
517
+
518
+ ```typescript
519
+ const router = createRouter();
520
+
521
+ // Users resource
522
+ router.get('/users', listUsers);
523
+ router.post('/users', createUser);
524
+ router.get('/users/:id', getUser);
525
+ router.put('/users/:id', updateUser);
526
+ router.delete('/users/:id', deleteUser);
527
+
528
+ app.route('/', router);
529
+ ```
530
+
531
+ ### API Versioning
532
+
533
+ ```typescript
534
+ const v1 = createRouter({ prefix: '/api/v1' });
535
+ v1.get('/users', v1UsersHandler);
536
+
537
+ const v2 = createRouter({ prefix: '/api/v2' });
538
+ v2.get('/users', v2UsersHandler);
539
+
540
+ app.route('/', v1);
541
+ app.route('/', v2);
542
+ ```
543
+
544
+ ### Catch-All for SPA
545
+
546
+ ```typescript
547
+ router.get('/api/*', apiHandler);
548
+
549
+ // Serve SPA for all other routes
550
+ router.get('/*', (ctx) => {
551
+ ctx.html(indexHtml);
552
+ });
553
+ ```
554
+
555
+ ## Common Mistakes
556
+
557
+ ### Forgetting to Mount Routes
558
+
559
+ ```typescript
560
+ // ❌ Routes not mounted
561
+ const router = createRouter();
562
+ router.get('/users', handler);
563
+ // Missing: app.route('/api', router)
564
+
565
+ // ✅ Recommended: Use app.route()
566
+ const router = createRouter();
567
+ router.get('/users', handler);
568
+ app.route('/api', router);
569
+
570
+ // ✅ Also works: Classic pattern
571
+ app.use(router.routes());
572
+ ```
573
+
574
+ ### Parameter Name & Value Case
575
+
576
+ ```typescript
577
+ // Parameter names preserve their original case from route definition
578
+ router.get('/users/:userId', (ctx) => {
579
+ // ✅ Original case is preserved
580
+ console.log(ctx.params.userId);
581
+ });
582
+
583
+ // Parameter values also preserve their original case
584
+ // GET /users/JohnDoe → ctx.params.userId === 'JohnDoe' (not 'johndoe')
585
+ ```
586
+
587
+ ### Wildcard Placement
588
+
589
+ ```typescript
590
+ // ❌ Wildcard not at end (won't work as expected)
591
+ router.get('/*/files', handler);
592
+
593
+ // ✅ Wildcard at end
594
+ router.get('/files/*', handler);
595
+ ```
596
+
597
+ ### Duplicate Route Registration
598
+
599
+ ```typescript
600
+ // ❌ Throws an error — same method + path registered twice
601
+ router.get('/users', handler1);
602
+ router.get('/users', handler2);
603
+ // Error: "Route conflict: GET /users is already registered"
604
+
605
+ // ✅ Different methods on the same path are fine
606
+ router.get('/users', handler1);
607
+ router.post('/users', handler2);
608
+ ```
609
+
610
+ ## Error Behavior
611
+
612
+ - **Duplicate routes**: Registering the same method + path throws immediately at registration time.
613
+ - **Unmatched routes**: `routes()` middleware sets `ctx.status = 404` and calls `next()`, allowing downstream middleware like `allowedMethods()` to respond.
614
+ - **Parameter name conflicts**: In development, a warning is logged when two routes define different parameter names at the same tree position. The first registered name takes precedence.
615
+
616
+ ## License
617
+
618
+ MIT