@nextrush/router 3.0.7 → 4.0.0-beta.1

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 (51) hide show
  1. package/README.md +250 -492
  2. package/dist/index.d.ts +227 -178
  3. package/dist/index.js +1075 -657
  4. package/dist/index.js.map +1 -1
  5. package/package.json +7 -6
  6. package/src/__tests__/allowed-methods.test.ts +144 -0
  7. package/src/__tests__/audit-fixes.test.ts +59 -0
  8. package/src/__tests__/canonical-path-security.test.ts +198 -0
  9. package/src/__tests__/canonicalize-memo.test.ts +108 -0
  10. package/src/__tests__/dispatch-deasync.test.ts +292 -0
  11. package/src/__tests__/find-node-differential.test.ts +220 -0
  12. package/src/__tests__/fixtures/match-golden.json +556 -0
  13. package/src/__tests__/head-auto-registration.test.ts +197 -0
  14. package/src/__tests__/helpers/differential-corpus.ts +314 -0
  15. package/src/__tests__/match-differential.test.ts +46 -0
  16. package/src/__tests__/match-hotpath-guard.test.ts +71 -0
  17. package/src/__tests__/match-node-indexed-unpooled.test.ts +83 -0
  18. package/src/__tests__/match-normalize-fastpath.test.ts +64 -0
  19. package/src/__tests__/match-prenormalized.test.ts +95 -0
  20. package/src/__tests__/match-safety.test.ts +223 -0
  21. package/src/__tests__/match-single-alloc.test.ts +82 -0
  22. package/src/__tests__/match-walk-pool-safety.test.ts +103 -0
  23. package/src/__tests__/middleware-pipeline.test.ts +306 -0
  24. package/src/__tests__/param-decoding.test.ts +78 -0
  25. package/src/__tests__/public-surface.test.ts +72 -0
  26. package/src/__tests__/registration-max-depth.test.ts +56 -0
  27. package/src/__tests__/route-metadata.test.ts +172 -0
  28. package/src/__tests__/router-audit.test.ts +315 -0
  29. package/src/__tests__/router-edge-cases.test.ts +2 -7
  30. package/src/__tests__/router.test.ts +148 -7
  31. package/src/__tests__/static-map-reset.test.ts +48 -0
  32. package/src/__tests__/walk-pool-sizing.test.ts +72 -0
  33. package/src/__tests__/walk-pool-undersized-guard.test.ts +59 -0
  34. package/src/canonicalize.ts +137 -0
  35. package/src/composition.ts +79 -0
  36. package/src/constants.ts +43 -0
  37. package/src/dispatch.ts +145 -0
  38. package/src/find-node.ts +125 -0
  39. package/src/group-router.ts +208 -0
  40. package/src/index.ts +14 -5
  41. package/src/match-route.ts +241 -0
  42. package/src/matching.ts +246 -0
  43. package/src/middleware-adapter.ts +59 -0
  44. package/src/redirect.ts +97 -0
  45. package/src/registration.ts +343 -0
  46. package/src/route-metadata.ts +68 -0
  47. package/src/router.ts +219 -872
  48. package/src/segment-trie.ts +227 -0
  49. package/src/state.ts +53 -0
  50. package/src/walk-pool.ts +208 -0
  51. package/src/radix-tree.ts +0 -184
package/README.md CHANGED
@@ -1,618 +1,376 @@
1
1
  # @nextrush/router
2
2
 
3
- > High-performance radix tree router for NextRush. O(k) route matching where k = path length, not route count.
3
+ > The routing engine behind NextRush a segment trie that matches a request to its handler in time proportional to the URL's depth, not the number of routes you've registered.
4
4
 
5
- ## The Problem
5
+ [![npm version](https://img.shields.io/npm/v/@nextrush/router.svg)](https://www.npmjs.com/package/@nextrush/router)
6
+ [![downloads](https://img.shields.io/npm/dm/@nextrush/router.svg)](https://www.npmjs.com/package/@nextrush/router)
7
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@nextrush/router.svg)](https://bundlephobia.com/package/@nextrush/router)
8
+ [![types](https://img.shields.io/npm/types/@nextrush/router.svg)](https://www.npmjs.com/package/@nextrush/router)
9
+ [![ESM only](https://img.shields.io/badge/module-ESM--only-blue.svg)](https://nodejs.org/api/esm.html)
10
+ [![license](https://img.shields.io/npm/l/@nextrush/router.svg)](https://github.com/0xTanzim/nextRush/blob/main/LICENSE)
6
11
 
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.
12
+ | | |
13
+ | --- | --- |
14
+ | **Purpose** | Route matching for NextRush — resolve `METHOD path` to its handler |
15
+ | **Package type** | Core |
16
+ | **Status** | Stable ✅ |
17
+ | **Included in `nextrush`?** | ✅ Yes — `createRouter` is re-exported from the meta package |
18
+ | **Support tier** | Public — core (stable, semver-guarded) — see [ADR-0005](https://github.com/0xTanzim/nextRush/blob/main/docs/adr/ADR-0005-package-tiers-sealed-surface-deprecation.md) |
19
+ | **Maintenance** | Active |
20
+ | **Runtime** | Universal — Node · Bun · Deno · Edge |
21
+ | **Requires** | Node `>=22` · ESM-only · TypeScript `>=5.x` |
22
+ | **Introduced** | `v3.0.0` |
8
23
 
9
- ## How NextRush Approaches This
24
+ ## Highlights
10
25
 
11
- The router uses a **radix tree** (compressed prefix tree):
26
+ - **No third-party dependencies** depends only on `@nextrush/types` (types, erased at build)
27
+ - ✅ **ESM-only**, tree-shakable, side-effect-free
28
+ - ✅ **Fully typed** — strict TypeScript, zero `any`
29
+ - 📦 Small, focused surface — the router and nothing else
12
30
 
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
31
+ <details>
32
+ <summary><strong>Table of contents</strong></summary>
17
33
 
18
- ## Mental Model
34
+ [The problem](#the-problem) · [When to use](#when-to-use) · [Installation](#installation) · [Quick start](#quick-start) · [Capabilities](#capabilities) · [Mental model](#mental-model) · [Common tasks](#common-tasks) · [API overview](#api-overview) · [Options](#options) · [Performance](#performance) · [Compatibility](#compatibility) · [Troubleshooting](#troubleshooting) · [FAQ](#faq) · [Package relationships](#package-relationships) · [Architecture](#architecture) · [Resources](#resources)
19
35
 
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
+ </details>
36
37
 
37
- When matching `/users/123/posts`:
38
+ ---
38
39
 
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
40
+ ## The problem
44
41
 
45
- ## Installation
42
+ The simplest router is a list: keep every route in an array and, on each request, walk the list until one matches. It works — until it doesn't. The cost of every request grows with the number of routes, and the *order* you register them in starts to change behavior.
46
43
 
47
- ```bash
48
- pnpm add @nextrush/router
44
+ ```ts
45
+ // TODAY, without a trie — a list router, O(n) in the route count:
46
+ for (const route of routes) {
47
+ if (route.method === method && route.pattern.test(path)) {
48
+ return route; // 1,000 routes → up to 1,000 regex tests, every request
49
+ }
50
+ }
49
51
  ```
50
52
 
51
- ## Quick Start
52
-
53
- ```typescript
54
- import { createApp } from '@nextrush/core';
55
- import { createRouter } from '@nextrush/router';
53
+ A list router that's fast with 20 routes is measurably slower with 2,000, and the slowdown lands on your hottest code path — the one every request pays. `@nextrush/router` removes the route count from that equation.
56
54
 
57
- const app = createApp();
58
- const router = createRouter();
55
+ ## When to use
59
56
 
60
- router.get('/', (ctx) => {
61
- ctx.json({ message: 'Home' });
62
- });
57
+ `@nextrush/router` is the router built into NextRush; `createApp().route(...)` uses it under the hood. You reach for this package directly when you want to compose routing explicitly.
63
58
 
64
- router.get('/users/:id', (ctx) => {
65
- ctx.json({ userId: ctx.params.id });
66
- });
59
+ **Use `@nextrush/router` if:**
67
60
 
68
- app.route('/', router);
69
- app.listen(3000);
70
- ```
61
+ - ✓ You're building a NextRush app and want feature routers you can mount, group, and nest
62
+ - ✓ You want matching cost to stay flat as your route table grows into the hundreds or thousands
63
+ - ✓ You need named params (`/users/:id`), wildcards (`/files/*`), per-route and per-group middleware, or redirects
71
64
 
72
- ## Features
65
+ **Reach for something else if:**
73
66
 
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`
67
+ - You just want the app-level shortcuts (`app.get(...)`) those already use this router; import from [`nextrush`](../nextrush) instead
68
+ - You need a standalone router for a non-NextRush framework — the `Router` speaks NextRush's `Context`/`Middleware` contracts from [`@nextrush/types`](../types)
81
69
 
82
- ## Route Patterns
70
+ ---
83
71
 
84
- ### Static Routes
72
+ ## Installation
85
73
 
86
- ```typescript
87
- router.get('/api/users', handler);
88
- router.get('/api/posts', handler);
89
- router.get('/health', handler);
74
+ ```bash
75
+ pnpm add @nextrush/router
76
+ # npm i @nextrush/router · yarn add @nextrush/router · bun add @nextrush/router
90
77
  ```
91
78
 
92
- ### Named Parameters
79
+ > [!NOTE]
80
+ > Already using `nextrush`? `createRouter` is re-exported from the meta package, so
81
+ > `import { createRouter } from 'nextrush'` works without installing this directly. Install
82
+ > `@nextrush/router` when you want to depend on it explicitly.
93
83
 
94
- ```typescript
95
- // Single parameter
96
- router.get('/users/:id', (ctx) => {
97
- ctx.json({ id: ctx.params.id });
98
- });
84
+ ## Quick start
99
85
 
100
- // Multiple parameters
101
- router.get('/users/:userId/posts/:postId', (ctx) => {
102
- const { userId, postId } = ctx.params;
103
- ctx.json({ userId, postId });
104
- });
105
- ```
86
+ ```ts
87
+ import { createApp, listen } from 'nextrush';
88
+ import { createRouter } from '@nextrush/router';
106
89
 
107
- ### Wildcard Routes
90
+ const app = createApp();
91
+ const users = createRouter();
108
92
 
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
- });
93
+ users.get('/', (ctx) => ctx.json([{ id: 1, name: 'Ada' }]));
94
+ users.get('/:id', (ctx) => ctx.json({ id: ctx.params.id }));
115
95
 
116
- // /files/docs/readme.md ctx.params['*'] = 'docs/readme.md'
96
+ app.route('/users', users); // mount the feature router
97
+ listen(app, 8080);
117
98
  ```
118
99
 
119
- Wildcards must be at the end of the route. Everything after the `*` segment is captured.
120
-
121
- ## HTTP Methods
100
+ A `Router` is a self-contained bundle of routes you register in one place and mount wherever you need it — here every route lands under `/users`. That's the whole model: build small routers, compose them.
122
101
 
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);
102
+ ## Capabilities
131
103
 
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);
104
+ **Routing**
105
+ - **Segment trie matching** O(k) in path depth, not route count (see [Mental model](#mental-model))
106
+ - **Named parameters** — `/users/:id`, multiple per path, original case preserved
107
+ - **Wildcards** — `/files/*` captures the remainder into `ctx.params['*']`
108
+ - **Route groups** — shared prefix + middleware, arbitrarily nestable
109
+ - **Composition** — `mount()` / `use()` sub-routers; `app.route()` for Hono-style mounting
110
+ - **Redirects** — `301`/`302`/`303`/`307`/`308` with param interpolation
111
+ - **Method handling** — per-method shortcuts plus `allowedMethods()` for correct `405`/`Allow`
135
112
 
136
- // Register specific method dynamically
137
- router.route('GET', '/dynamic', handler);
138
- ```
113
+ **Performance**
114
+ - **Static-route fast path** — routes with no params resolve through an O(1) hash map
115
+ - **Compiled executors** — the middleware+handler chain is built once at registration, not per request
139
116
 
140
- ## Route Groups
117
+ **Developer experience**
118
+ - **Introspection** — `getRoutes()` feeds `@nextrush/openapi` without touching the hot path
119
+ - **Fully typed** — strict TypeScript, zero `any`; contracts shared via `@nextrush/types`
141
120
 
142
- Group routes with a common prefix:
121
+ ## Mental model
143
122
 
144
- ```typescript
145
- const api = createRouter({ prefix: '/api/v1' });
123
+ A segment trie is a tree keyed by **whole path segments** — `users`, `:id` — not by individual characters (that would be a radix tree; this is deliberately not one). Registering a route walks the tree segment by segment, creating nodes as needed. Matching walks the same tree, and the depth of the walk is the number of segments in the URL — so a thousand sibling routes cost the same as one.
146
124
 
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
125
+ ```text
126
+ Routes registered: Trie built:
127
+ /users (root)
128
+ /users/:id ├── "users" ← static child
129
+ /users/:id/posts │ └── :id ← param child
130
+ /products │ └── "posts"
131
+ /products/:id └── "products"
132
+ └── :id
150
133
 
151
- app.route('/', api);
134
+ Matching GET /users/123/posts:
135
+ root → "users" → :id (captures id="123") → "posts" → handler + { id: "123" }
152
136
  ```
153
137
 
154
- ### Nested Groups
138
+ **Rule:** static routes (no `:param`, no `*`) skip the walk entirely — they live in a method-nested hash map for an O(1) lookup.
155
139
 
156
- ```typescript
157
- const router = createRouter();
140
+ > [!TIP]
141
+ > The full match lifecycle, the two-path design, and the executor-compilation trick are in
142
+ > [`ARCHITECTURE.md`](./ARCHITECTURE.md).
158
143
 
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
- });
144
+ ---
164
145
 
165
- api.group('/v2', (v2) => {
166
- v2.get('/users', handler); // GET /api/v2/users
167
- });
168
- });
169
-
170
- app.route('/', router);
171
- ```
146
+ ## Common tasks
172
147
 
173
- ### Group Middleware
148
+ ### Route parameters and wildcards
174
149
 
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
- };
150
+ ```ts
151
+ router.get('/users/:userId/posts/:postId', (ctx) => {
152
+ const { userId, postId } = ctx.params; // both captured in one pass
153
+ ctx.json({ userId, postId });
154
+ });
186
155
 
187
- router.group('/admin', [auth], (admin) => {
188
- admin.get('/dashboard', getDashboard);
189
- admin.get('/settings', getSettings);
156
+ // Wildcard captures the remainder; must be the last segment.
157
+ router.get('/files/*', (ctx) => {
158
+ ctx.json({ path: ctx.params['*'] }); // /files/docs/readme.md → "docs/readme.md"
190
159
  });
191
160
  ```
192
161
 
193
- ## Route Middleware
162
+ ### Route and group middleware
194
163
 
195
- Apply middleware to specific routes:
196
-
197
- ```typescript
198
- const auth = async (ctx, next) => {
199
- if (!ctx.get('Authorization')) {
164
+ ```ts
165
+ const auth: Middleware = async (ctx, next) => {
166
+ if (!ctx.get('authorization')) {
200
167
  ctx.status = 401;
201
- ctx.json({ error: 'Unauthorized' });
202
- return;
168
+ return ctx.json({ error: 'Unauthorized' });
203
169
  }
204
170
  await next();
205
171
  };
206
172
 
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`
173
+ router.get('/protected', auth, (ctx) => ctx.json({ ok: true })); // per-route
215
174
 
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
175
+ router.group('/admin', [auth], (admin) => { // per-group
176
+ admin.get('/dashboard', (ctx) => ctx.json({ page: 'dashboard' }));
177
+ });
233
178
  ```
234
179
 
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)
180
+ Middleware runs in registration order, then the handler: `auth → handler`.
242
181
 
243
- ```typescript
244
- const userRouter = createRouter();
245
- userRouter.get('/', listUsers);
246
- userRouter.get('/:id', getUser);
247
- userRouter.post('/', createUser);
182
+ ### Composing routers
248
183
 
249
- const postRouter = createRouter();
250
- postRouter.get('/', listPosts);
251
- postRouter.get('/:id', getPost);
184
+ ```ts
185
+ const usersRouter = createRouter();
186
+ usersRouter.get('/', listUsers);
187
+ usersRouter.get('/:id', getUser);
252
188
 
253
189
  const api = createRouter();
254
- api.mount('/users', userRouter); // Clean explicit mounting
255
- api.mount('/posts', postRouter);
190
+ api.mount('/users', usersRouter); // explicit; equivalent to use('/users', usersRouter)
256
191
 
257
- app.route('/api', api);
258
- // Results in: /api/users, /api/users/:id, /api/posts, /api/posts/:id
192
+ app.route('/api', api); // → /api/users, /api/users/:id
259
193
  ```
260
194
 
261
- ### Using use() (Classic Pattern)
195
+ ### Redirects and method handling
262
196
 
263
- The traditional `use()` method also works:
197
+ ```ts
198
+ router.redirect('/old', '/new'); // 301 by default
199
+ router.redirect('/users/:id', '/people/:id'); // param interpolation
200
+ router.redirect('/legacy', '/v2', 308); // 307/308 preserve the HTTP method
264
201
 
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
202
+ app.use(router.allowedMethods()); // OPTIONS → Allow header; unknown method on a known path → 405
297
203
  ```
298
204
 
299
- ## Router Options
205
+ ## API overview
300
206
 
301
- ```typescript
302
- interface RouterOptions {
303
- // Route prefix prepended to all routes
304
- prefix?: string;
207
+ The sealed public surface (ADR-0005). Advanced trie internals are exported for tooling but are not part of the everyday API.
305
208
 
306
- // Case sensitive matching (default: false)
307
- caseSensitive?: boolean;
209
+ | Export | Signature | Since | Stability | Description |
210
+ | ------ | --------- | ----- | --------- | ----------- |
211
+ | `createRouter` | `(options?: RouterOptions) => Router` | `3.0.0` | Stable ✅ | Create a router instance. |
212
+ | `Router` | `class` | `3.0.0` | Stable ✅ | The router class (see methods below). |
213
+ | `endpoint` | `(metadata) => RouteEntry` | `3.1.0` | Stable ✅ | Attach inline route metadata (consumed by `getRoutes()` / OpenAPI). |
214
+ | `createNode` | `(segment, type?) => TrieNode` | `3.0.0` | Advanced 🔧 | Build a trie node (tooling). |
215
+ | `parseSegments` | `(path, caseSensitive?) => ParsedSegment[]` | `3.0.0` | Advanced 🔧 | Split a path into typed segments (tooling). |
216
+ | `NodeType` | `const enum` | `3.0.0` | Advanced 🔧 | `STATIC` / `PARAM` / `WILDCARD`. |
217
+ | `type Router` (as `RouterInterface`) · `RouterOptions` · `Route` · `RouteHandler` · `RouteMatch` · `Middleware` · `HttpMethod` | — | `3.0.0` | Stable ✅ | Re-exported contracts from [`@nextrush/types`](../types). |
218
+ | `type TrieNode` · `HandlerEntry` · `ParsedSegment` · `RouteGroup` | — | `3.0.0` | Advanced 🔧 | Trie/group types. |
308
219
 
309
- // Strict trailing slash handling (default: false)
310
- strict?: boolean;
311
- }
220
+ ### `Router` methods
312
221
 
313
- const router = createRouter({
314
- prefix: '/api/v1',
315
- caseSensitive: false,
316
- strict: false,
317
- });
318
- ```
222
+ | Method | Signature | Description |
223
+ | ------ | --------- | ----------- |
224
+ | `get` / `post` / `put` / `delete` / `patch` / `head` / `options` | `(path, ...entries: RouteEntry[]) => this` | Register a handler (+ optional per-route middleware) for one method. |
225
+ | `all` | `(path, ...entries) => this` | Register for every standard method (GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS); one consolidated introspection row. |
226
+ | `route` | `(method, path, ...entries) => this` | Register for a method chosen at runtime. |
227
+ | `group` | `(prefix, callback)` or `(prefix, middleware[], callback) => this` | Register routes under a shared prefix (and optional middleware); nestable. |
228
+ | `mount` | `(path, subRouter) => this` | Mount a sub-router at a prefix (explicit form of `use(path, subRouter)`). |
229
+ | `use` | `(middleware)` · `(path, subRouter)` · `(subRouter) => this` | Add router-level middleware or mount a sub-router. |
230
+ | `redirect` | `(from, to, status?: 301\|302\|303\|307\|308) => this` | Register a redirect (default `301`; `307`/`308` preserve the method). |
231
+ | `match` | `(method, path) => RouteMatch \| null` | Resolve a request to its handler + params (used internally by `routes()`). |
232
+ | `routes` | `() => Middleware` | The dispatch middleware to mount on the app (`app.use(router.routes())`). |
233
+ | `allowedMethods` | `() => Middleware` | Middleware that answers `OPTIONS` and returns `405` for a known path + unknown method. |
234
+ | `getRoutes` | `() => readonly RouteDefinition[]` | Introspection registry (doc/OpenAPI generation) — never read on the request path. |
235
+ | `reset` | `() => void` | Clear all routes, middleware, and caches; makes the router reusable (tests, hot reload). |
319
236
 
320
- ### Case Sensitivity
237
+ ## Options
321
238
 
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
239
+ ```ts
240
+ const router = createRouter({ prefix: '/api/v1', caseSensitive: false, strict: false });
333
241
  ```
334
242
 
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
- ```
243
+ | Option | Type | Required | Default | Security-sensitive | Description |
244
+ | ------ | ---- | -------- | ------- | ------------------ | ----------- |
245
+ | `prefix` | `string` | No | `''` | — | Prepended to every route registered on this router. |
246
+ | `caseSensitive` | `boolean` | No | `false` | — | When `false`, static segments are lowercased at registration and match case-insensitively. Parameter **names and values always preserve their original case.** |
247
+ | `strict` | `boolean` | No | `false` | — | When `false`, a trailing slash is normalized away (`/users` and `/users/` match the same route). |
343
248
 
344
249
  ## Performance
345
250
 
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
251
+ The router is designed so that **match cost tracks URL depth, not route count** — the property that matters as an app grows. Two mechanisms deliver it:
391
252
 
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
- ```
253
+ - **Static routes** (no `:param` / `*`) resolve through a method-nested hash map — an O(1) lookup, with no per-request `` `${method} ${path}` `` key string allocated.
254
+ - **Dynamic routes** walk the segment trie: O(k) where `k` is the number of path segments (typically a handful), regardless of how many routes are registered.
403
255
 
404
- #### `router.redirect(from, to, status?)`
256
+ Per-route **executors are compiled once at registration**, not rebuilt per request, so dispatch allocates no middleware-chain closures on the hot path. A router with only static routes skips the trie walk entirely.
405
257
 
406
- Register a redirect route.
258
+ > [!NOTE]
259
+ > For reproducible throughput numbers on your own hardware, run the suite in
260
+ > [`apps/benchmark`](https://github.com/0xTanzim/nextRush/tree/main/apps/benchmark). Published
261
+ > figures are being re-measured on a hardened harness — see the
262
+ > [root README's Performance note](https://github.com/0xTanzim/nextRush#performance) — so this
263
+ > package documents the complexity characteristics, not point numbers.
407
264
 
408
- ```typescript
409
- router.redirect(from: string, to: string, status?: 301 | 302 | 303 | 307 | 308): this
410
- ```
265
+ ## Compatibility
411
266
 
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.
267
+ **Requirements**
413
268
 
414
- #### `router.group(prefix, middleware?, callback)`
269
+ | Requirement | Version |
270
+ | ----------- | ------- |
271
+ | NextRush | `3.x` |
272
+ | Node.js | `>=22` |
273
+ | TypeScript | `>=5.x` |
415
274
 
416
- Create a route group with shared prefix and middleware.
275
+ **Runtimes**
417
276
 
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
- ```
277
+ | Runtime | Supported | Notes |
278
+ | ------- | --------- | ----- |
279
+ | Node.js `>=22` | | ESM-only |
280
+ | Bun / Deno / Edge | ✅ / ✅ / ✅ | Pure Web-standard logic — no runtime API used; parity held by `@nextrush/adapter-*` |
422
281
 
423
- #### `router.use(pathOrMiddleware, routerOrUndefined?)`
282
+ **Integration**
283
+ - **Peer dependencies:** `@nextrush/core` (optional) — only needed when you mount via `app.route()` / `app.use()`; the `Router` itself works standalone.
284
+ - **Works with:** `@nextrush/core` (mounting), `@nextrush/openapi` (reads `getRoutes()`), any `@nextrush/*` middleware.
285
+ - **Incompatible with:** none.
424
286
 
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
- ```
287
+ > [!IMPORTANT]
288
+ > NextRush is **ESM-only, permanently** — no CommonJS build. On Node `>=22`, CommonJS consumers
289
+ > can `require()` this ESM package natively. See the
290
+ > [Module Format Policy](https://github.com/0xTanzim/nextRush#module-format-policy).
440
291
 
441
- **Example:**
292
+ ---
442
293
 
443
- ```typescript
444
- const api = createRouter();
445
- api.mount('/users', userRouter);
446
- api.mount('/posts', postRouter);
447
- ```
294
+ ## Troubleshooting
448
295
 
449
- #### `router.match(method, path)`
296
+ <details>
297
+ <summary><strong><code>Error: Route conflict: GET /users is already registered</code></strong></summary>
450
298
 
451
- Match a route and return handler + params.
299
+ **Cause:** the same method + path was registered twice — conflicts throw at registration time, not silently last-wins. **Fix:** register each method/path once; different methods on the same path are fine.
452
300
 
453
- ```typescript
454
- router.match(method: HttpMethod, path: string): RouteMatch | null
301
+ ```ts
302
+ router.get('/users', listUsers);
303
+ router.post('/users', createUser); // ✅ different method — OK
304
+ // router.get('/users', other); // ❌ throws
455
305
  ```
456
306
 
457
- **Returns:**
307
+ </details>
458
308
 
459
- ```typescript
460
- interface RouteMatch {
461
- handler: RouteHandler;
462
- params: Record<string, string>;
463
- middleware: Middleware[];
464
- }
465
- ```
309
+ <details>
310
+ <summary><strong>My route isn't matched (404)</strong></summary>
466
311
 
467
- #### `router.reset()`
312
+ **Cause:** the router was created but never mounted. **Fix:** mount it with `app.route(prefix, router)` (or `app.use(router.routes())`).
468
313
 
469
- Clear all registered routes, middleware, and internal state. Useful for testing or plugin teardown.
470
-
471
- ```typescript
314
+ ```ts
472
315
  const router = createRouter();
473
316
  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);
317
+ app.route('/', router); // ← without this, nothing is matched
529
318
  ```
530
319
 
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
- ```
320
+ </details>
554
321
 
555
- ## Common Mistakes
322
+ <details>
323
+ <summary><strong>A wildcard in the middle of a path doesn't work</strong></summary>
556
324
 
557
- ### Forgetting to Mount Routes
325
+ **Cause:** a wildcard captures the remainder of the path, so it's only valid as the **last** segment. **Fix:** put `*` at the end (`/files/*`), not in the middle (`/*/files`).
558
326
 
559
- ```typescript
560
- // ❌ Routes not mounted
561
- const router = createRouter();
562
- router.get('/users', handler);
563
- // Missing: app.route('/api', router)
327
+ </details>
564
328
 
565
- // ✅ Recommended: Use app.route()
566
- const router = createRouter();
567
- router.get('/users', handler);
568
- app.route('/api', router);
329
+ ## FAQ
569
330
 
570
- // Also works: Classic pattern
571
- app.use(router.routes());
572
- ```
331
+ **Can I use `@nextrush/router` without the rest of NextRush?**
332
+ Yes. The `Router` depends only on the `Context`/`Middleware` type contracts from `@nextrush/types`. `@nextrush/core` is an *optional* peer — you need it only to mount via `app.route()`.
573
333
 
574
- ### Parameter Name & Value Case
334
+ **Is it really a "radix tree"?**
335
+ No — and the distinction is deliberate. This is a **segment trie**: it branches on whole path segments (`users`, `:id`), not individual characters. A radix tree (character/prefix-compressed) is a different structure; an opt-in `@nextrush/router-radix` is explored in [RFC-015](https://github.com/0xTanzim/nextRush/blob/main/docs/RFC/runtime-adapters/015-router-radix.md), but the segment trie is the default.
575
336
 
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
- });
337
+ **Why ESM-only?**
338
+ See the [Module Format Policy](https://github.com/0xTanzim/nextRush#module-format-policy).
582
339
 
583
- // Parameter values also preserve their original case
584
- // GET /users/JohnDoe ctx.params.userId === 'JohnDoe' (not 'johndoe')
585
- ```
340
+ **Does it work on Bun, Deno, and Edge?**
341
+ Yes. The matching logic uses only standard JavaScript — no `node:*` APIs — so it runs identically on every supported runtime; cross-runtime parity is enforced by `@nextrush/adapter-*` conformance tests.
586
342
 
587
- ### Wildcard Placement
343
+ ---
588
344
 
589
- ```typescript
590
- // ❌ Wildcard not at end (won't work as expected)
591
- router.get('/*/files', handler);
345
+ ## Package relationships
592
346
 
593
- // ✅ Wildcard at end
594
- router.get('/files/*', handler);
347
+ ```text
348
+ depends on @nextrush/types (Context / Middleware / RouteMatch contracts)
349
+ @nextrush/router ─────────────▶
350
+ often used with @nextrush/core (mounts routers via app.route())
351
+ usually used next @nextrush/openapi (generates a spec from getRoutes())
595
352
  ```
596
353
 
597
- ### Duplicate Route Registration
354
+ - **Depends on:** [`@nextrush/types`](../types) — the shared `Context` / `Middleware` / `RouteMatch` contracts (types only, erased at build).
355
+ - **Often used with:** [`@nextrush/core`](../core) — the `Application` that mounts routers via `app.route()`.
356
+ - **Usually used next:** [`@nextrush/openapi`](../middleware/openapi) — turns `getRoutes()` into an OpenAPI document · [`@nextrush/validation`](../middleware/validation) · [`@nextrush/class`](../class) (decorator-based routing).
357
+ - **Alternative:** an opt-in `@nextrush/router-radix` is explored in [RFC-015](https://github.com/0xTanzim/nextRush/blob/main/docs/RFC/runtime-adapters/015-router-radix.md) for very large static route tables; the segment trie is the default.
598
358
 
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"
359
+ ## Architecture
604
360
 
605
- // Different methods on the same path are fine
606
- router.get('/users', handler1);
607
- router.post('/users', handler2);
608
- ```
361
+ Maintaining or contributing to the router? The internal design — the two-path match, the segment
362
+ trie and static map, executor compilation, the architectural invariants, and the decisions and
363
+ trade-offs behind them (with diagrams) — is in **[`ARCHITECTURE.md`](./ARCHITECTURE.md)**. Design
364
+ history: [RFC-015 (router-radix)](https://github.com/0xTanzim/nextRush/blob/main/docs/RFC/runtime-adapters/015-router-radix.md), [ADR-0005 (package tiers)](https://github.com/0xTanzim/nextRush/blob/main/docs/adr/ADR-0005-package-tiers-sealed-surface-deprecation.md).
609
365
 
610
- ## Error Behavior
366
+ ## Resources
611
367
 
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.
368
+ - 📖 **Learn** [Documentation](https://0xtanzim.github.io/nextRush/docs) · [Routing concept](https://0xtanzim.github.io/nextRush/docs/concepts/routing) · [Architecture](./ARCHITECTURE.md) · [RFCs](https://github.com/0xTanzim/nextRush/tree/main/docs/RFC)
369
+ - 📝 **Changelog** [CHANGELOG.md](./CHANGELOG.md)
370
+ - 🧪 **Benchmarks** [`apps/benchmark`](https://github.com/0xTanzim/nextRush/tree/main/apps/benchmark)
371
+ - 🐛 **Report an issue** — [GitHub Issues](https://github.com/0xTanzim/nextRush/issues)
372
+ - 🤝 **Contribute** — [CONTRIBUTING.md](https://github.com/0xTanzim/nextRush/blob/main/CONTRIBUTING.md)
615
373
 
616
- ## License
374
+ ---
617
375
 
618
- MIT
376
+ MIT © [Tanzim Hossain](https://github.com/0xTanzim)