@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 +21 -0
- package/README.md +618 -0
- package/dist/index.d.ts +301 -0
- package/dist/index.js +853 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/src/__tests__/router-edge-cases.test.ts +486 -0
- package/src/__tests__/router.test.ts +621 -0
- package/src/index.ts +31 -0
- package/src/radix-tree.ts +184 -0
- package/src/router.ts +1029 -0
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
|