@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.
- package/README.md +250 -492
- package/dist/index.d.ts +227 -178
- package/dist/index.js +1075 -657
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/__tests__/allowed-methods.test.ts +144 -0
- package/src/__tests__/audit-fixes.test.ts +59 -0
- package/src/__tests__/canonical-path-security.test.ts +198 -0
- package/src/__tests__/canonicalize-memo.test.ts +108 -0
- package/src/__tests__/dispatch-deasync.test.ts +292 -0
- package/src/__tests__/find-node-differential.test.ts +220 -0
- package/src/__tests__/fixtures/match-golden.json +556 -0
- package/src/__tests__/head-auto-registration.test.ts +197 -0
- package/src/__tests__/helpers/differential-corpus.ts +314 -0
- package/src/__tests__/match-differential.test.ts +46 -0
- package/src/__tests__/match-hotpath-guard.test.ts +71 -0
- package/src/__tests__/match-node-indexed-unpooled.test.ts +83 -0
- package/src/__tests__/match-normalize-fastpath.test.ts +64 -0
- package/src/__tests__/match-prenormalized.test.ts +95 -0
- package/src/__tests__/match-safety.test.ts +223 -0
- package/src/__tests__/match-single-alloc.test.ts +82 -0
- package/src/__tests__/match-walk-pool-safety.test.ts +103 -0
- package/src/__tests__/middleware-pipeline.test.ts +306 -0
- package/src/__tests__/param-decoding.test.ts +78 -0
- package/src/__tests__/public-surface.test.ts +72 -0
- package/src/__tests__/registration-max-depth.test.ts +56 -0
- package/src/__tests__/route-metadata.test.ts +172 -0
- package/src/__tests__/router-audit.test.ts +315 -0
- package/src/__tests__/router-edge-cases.test.ts +2 -7
- package/src/__tests__/router.test.ts +148 -7
- package/src/__tests__/static-map-reset.test.ts +48 -0
- package/src/__tests__/walk-pool-sizing.test.ts +72 -0
- package/src/__tests__/walk-pool-undersized-guard.test.ts +59 -0
- package/src/canonicalize.ts +137 -0
- package/src/composition.ts +79 -0
- package/src/constants.ts +43 -0
- package/src/dispatch.ts +145 -0
- package/src/find-node.ts +125 -0
- package/src/group-router.ts +208 -0
- package/src/index.ts +14 -5
- package/src/match-route.ts +241 -0
- package/src/matching.ts +246 -0
- package/src/middleware-adapter.ts +59 -0
- package/src/redirect.ts +97 -0
- package/src/registration.ts +343 -0
- package/src/route-metadata.ts +68 -0
- package/src/router.ts +219 -872
- package/src/segment-trie.ts +227 -0
- package/src/state.ts +53 -0
- package/src/walk-pool.ts +208 -0
- package/src/radix-tree.ts +0 -184
package/README.md
CHANGED
|
@@ -1,618 +1,376 @@
|
|
|
1
1
|
# @nextrush/router
|
|
2
2
|
|
|
3
|
-
>
|
|
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
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@nextrush/router)
|
|
6
|
+
[](https://www.npmjs.com/package/@nextrush/router)
|
|
7
|
+
[](https://bundlephobia.com/package/@nextrush/router)
|
|
8
|
+
[](https://www.npmjs.com/package/@nextrush/router)
|
|
9
|
+
[](https://nodejs.org/api/esm.html)
|
|
10
|
+
[](https://github.com/0xTanzim/nextRush/blob/main/LICENSE)
|
|
6
11
|
|
|
7
|
-
|
|
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
|
-
##
|
|
24
|
+
## Highlights
|
|
10
25
|
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
+
---
|
|
38
39
|
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
const router = createRouter();
|
|
55
|
+
## When to use
|
|
59
56
|
|
|
60
|
-
router
|
|
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
|
|
65
|
-
ctx.json({ userId: ctx.params.id });
|
|
66
|
-
});
|
|
59
|
+
**Use `@nextrush/router` if:**
|
|
67
60
|
|
|
68
|
-
app
|
|
69
|
-
|
|
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
|
-
|
|
65
|
+
**Reach for something else if:**
|
|
73
66
|
|
|
74
|
-
-
|
|
75
|
-
-
|
|
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
|
-
|
|
70
|
+
---
|
|
83
71
|
|
|
84
|
-
|
|
72
|
+
## Installation
|
|
85
73
|
|
|
86
|
-
```
|
|
87
|
-
router
|
|
88
|
-
router
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
// Single parameter
|
|
96
|
-
router.get('/users/:id', (ctx) => {
|
|
97
|
-
ctx.json({ id: ctx.params.id });
|
|
98
|
-
});
|
|
84
|
+
## Quick start
|
|
99
85
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
ctx.json({ userId, postId });
|
|
104
|
-
});
|
|
105
|
-
```
|
|
86
|
+
```ts
|
|
87
|
+
import { createApp, listen } from 'nextrush';
|
|
88
|
+
import { createRouter } from '@nextrush/router';
|
|
106
89
|
|
|
107
|
-
|
|
90
|
+
const app = createApp();
|
|
91
|
+
const users = createRouter();
|
|
108
92
|
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
//
|
|
96
|
+
app.route('/users', users); // mount the feature router
|
|
97
|
+
listen(app, 8080);
|
|
117
98
|
```
|
|
118
99
|
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
## Mental model
|
|
143
122
|
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
134
|
+
Matching GET /users/123/posts:
|
|
135
|
+
root → "users" → :id (captures id="123") → "posts" → handler + { id: "123" }
|
|
152
136
|
```
|
|
153
137
|
|
|
154
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
v2.get('/users', handler); // GET /api/v2/users
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
app.route('/', router);
|
|
171
|
-
```
|
|
146
|
+
## Common tasks
|
|
172
147
|
|
|
173
|
-
###
|
|
148
|
+
### Route parameters and wildcards
|
|
174
149
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
162
|
+
### Route and group middleware
|
|
194
163
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
const userRouter = createRouter();
|
|
245
|
-
userRouter.get('/', listUsers);
|
|
246
|
-
userRouter.get('/:id', getUser);
|
|
247
|
-
userRouter.post('/', createUser);
|
|
182
|
+
### Composing routers
|
|
248
183
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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',
|
|
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
|
-
###
|
|
195
|
+
### Redirects and method handling
|
|
262
196
|
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
205
|
+
## API overview
|
|
300
206
|
|
|
301
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
310
|
-
strict?: boolean;
|
|
311
|
-
}
|
|
220
|
+
### `Router` methods
|
|
312
221
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
237
|
+
## Options
|
|
321
238
|
|
|
322
|
-
```
|
|
323
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
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
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
409
|
-
router.redirect(from: string, to: string, status?: 301 | 302 | 303 | 307 | 308): this
|
|
410
|
-
```
|
|
265
|
+
## Compatibility
|
|
411
266
|
|
|
412
|
-
|
|
267
|
+
**Requirements**
|
|
413
268
|
|
|
414
|
-
|
|
269
|
+
| Requirement | Version |
|
|
270
|
+
| ----------- | ------- |
|
|
271
|
+
| NextRush | `3.x` |
|
|
272
|
+
| Node.js | `>=22` |
|
|
273
|
+
| TypeScript | `>=5.x` |
|
|
415
274
|
|
|
416
|
-
|
|
275
|
+
**Runtimes**
|
|
417
276
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
292
|
+
---
|
|
442
293
|
|
|
443
|
-
|
|
444
|
-
const api = createRouter();
|
|
445
|
-
api.mount('/users', userRouter);
|
|
446
|
-
api.mount('/posts', postRouter);
|
|
447
|
-
```
|
|
294
|
+
## Troubleshooting
|
|
448
295
|
|
|
449
|
-
|
|
296
|
+
<details>
|
|
297
|
+
<summary><strong><code>Error: Route conflict: GET /users is already registered</code></strong></summary>
|
|
450
298
|
|
|
451
|
-
|
|
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
|
-
```
|
|
454
|
-
router.
|
|
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
|
-
|
|
307
|
+
</details>
|
|
458
308
|
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
+
<details>
|
|
323
|
+
<summary><strong>A wildcard in the middle of a path doesn't work</strong></summary>
|
|
556
324
|
|
|
557
|
-
|
|
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
|
-
|
|
560
|
-
// ❌ Routes not mounted
|
|
561
|
-
const router = createRouter();
|
|
562
|
-
router.get('/users', handler);
|
|
563
|
-
// Missing: app.route('/api', router)
|
|
327
|
+
</details>
|
|
564
328
|
|
|
565
|
-
|
|
566
|
-
const router = createRouter();
|
|
567
|
-
router.get('/users', handler);
|
|
568
|
-
app.route('/api', router);
|
|
329
|
+
## FAQ
|
|
569
330
|
|
|
570
|
-
|
|
571
|
-
app.
|
|
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
|
-
|
|
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
|
-
|
|
577
|
-
|
|
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
|
-
|
|
584
|
-
|
|
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
|
-
|
|
343
|
+
---
|
|
588
344
|
|
|
589
|
-
|
|
590
|
-
// ❌ Wildcard not at end (won't work as expected)
|
|
591
|
-
router.get('/*/files', handler);
|
|
345
|
+
## Package relationships
|
|
592
346
|
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
##
|
|
366
|
+
## Resources
|
|
611
367
|
|
|
612
|
-
- **
|
|
613
|
-
- **
|
|
614
|
-
- **
|
|
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
|
-
|
|
374
|
+
---
|
|
617
375
|
|
|
618
|
-
MIT
|
|
376
|
+
MIT © [Tanzim Hossain](https://github.com/0xTanzim)
|