@koa/router 13.1.1 → 15.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 +1 -1
- package/README.md +1136 -41
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1378 -0
- package/dist/index.mjs +1341 -0
- package/dist/layer.d.mts +761 -0
- package/dist/layer.d.ts +761 -0
- package/dist/layer.js +457 -0
- package/dist/layer.mjs +436 -0
- package/dist/router.d.mts +3 -0
- package/dist/router.d.ts +3 -0
- package/dist/router.js +1371 -0
- package/dist/router.mjs +1340 -0
- package/dist/types.d.mts +3 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.js +18 -0
- package/dist/types.mjs +0 -0
- package/package.json +58 -30
- package/lib/API_tpl.hbs +0 -7
- package/lib/layer.js +0 -240
- package/lib/router.js +0 -824
package/README.md
CHANGED
|
@@ -1,67 +1,1166 @@
|
|
|
1
1
|
# [@koa/router](https://github.com/koajs/router)
|
|
2
2
|
|
|
3
|
-
> Router middleware for [Koa](https://github.com/koajs/koa). Maintained by [Forward Email][forward-email] and [Lad][].
|
|
3
|
+
> Modern TypeScript Router middleware for [Koa](https://github.com/koajs/koa). Maintained by [Forward Email][forward-email] and [Lad][].
|
|
4
4
|
|
|
5
5
|
[](https://github.com/koajs/router/actions/workflows/ci.yml)
|
|
6
|
-
[](https://github.com/sindresorhus/xo)
|
|
7
6
|
[](https://github.com/prettier/prettier)
|
|
8
7
|
[](https://lass.js.org)
|
|
9
8
|
[](LICENSE)
|
|
10
9
|
|
|
11
|
-
|
|
12
10
|
## Table of Contents
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
- [Features](#features)
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [TypeScript Support](#typescript-support)
|
|
15
|
+
- [Quick Start](#quick-start)
|
|
16
|
+
- [API Documentation](#api-documentation)
|
|
17
|
+
- [Advanced Features](#advanced-features)
|
|
18
|
+
- [Best Practices](#best-practices)
|
|
19
|
+
- [Recipes](#recipes)
|
|
20
|
+
- [Performance](#performance)
|
|
21
|
+
- [Testing](#testing)
|
|
22
|
+
- [Migration Guides](#migration-guides)
|
|
23
|
+
- [Contributing](#contributing)
|
|
24
|
+
- [License](#license)
|
|
25
|
+
- [Contributors](#contributors)
|
|
22
26
|
|
|
23
27
|
## Features
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
- ✅ **Full TypeScript Support** - Written in TypeScript with comprehensive type definitions
|
|
30
|
+
- ✅ **Express-Style Routing** - Familiar `app.get`, `app.post`, `app.put`, etc.
|
|
31
|
+
- ✅ **Named URL Parameters** - Extract parameters from URLs
|
|
32
|
+
- ✅ **Named Routes** - Generate URLs from route names
|
|
33
|
+
- ✅ **Host Matching** - Match routes based on hostname
|
|
34
|
+
- ✅ **HEAD Request Support** - Automatic HEAD support for GET routes
|
|
35
|
+
- ✅ **Multiple Middleware** - Chain multiple middleware functions
|
|
36
|
+
- ✅ **Nested Routers** - Mount routers within routers
|
|
37
|
+
- ✅ **RegExp Paths** - Use regular expressions for flexible path matching
|
|
38
|
+
- ✅ **Parameter Middleware** - Run middleware for specific URL parameters
|
|
39
|
+
- ✅ **Path-to-RegExp v8** - Modern, predictable path matching
|
|
40
|
+
- ✅ **405 Method Not Allowed** - Automatic method validation
|
|
41
|
+
- ✅ **501 Not Implemented** - Proper HTTP status codes
|
|
42
|
+
- ✅ **Async/Await** - Full promise-based middleware support
|
|
34
43
|
|
|
44
|
+
## Installation
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
**npm:**
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
* Middleware is now always run in the order declared by `.use()` (or `.get()`,
|
|
42
|
-
etc.), which matches Express 4 API.
|
|
48
|
+
```bash
|
|
49
|
+
npm install @koa/router
|
|
50
|
+
```
|
|
43
51
|
|
|
52
|
+
**yarn:**
|
|
44
53
|
|
|
45
|
-
|
|
54
|
+
```bash
|
|
55
|
+
yarn add @koa/router
|
|
56
|
+
```
|
|
46
57
|
|
|
47
|
-
|
|
58
|
+
**Requirements:**
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
- Node.js >= 20 (tested on v20, v22, v24, v25)
|
|
61
|
+
- Koa >= 2.0.0
|
|
62
|
+
|
|
63
|
+
## TypeScript Support
|
|
64
|
+
|
|
65
|
+
@koa/router is written in TypeScript and includes comprehensive type definitions out of the box. No need for `@types/*` packages!
|
|
66
|
+
|
|
67
|
+
### Basic Usage
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import Router, { RouterContext } from '@koa/router';
|
|
71
|
+
|
|
72
|
+
const router = new Router();
|
|
73
|
+
|
|
74
|
+
// Fully typed context
|
|
75
|
+
router.get('/:id', (ctx: RouterContext, next) => {
|
|
76
|
+
const id = ctx.params.id; // Type-safe parameters
|
|
77
|
+
ctx.body = { id };
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Generic Types
|
|
82
|
+
|
|
83
|
+
The router supports generic type parameters for full type safety with custom state and context types:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import Router, { RouterContext } from '@koa/router';
|
|
87
|
+
import type { Next } from 'koa';
|
|
88
|
+
|
|
89
|
+
// Define your application state
|
|
90
|
+
interface AppState {
|
|
91
|
+
user?: {
|
|
92
|
+
id: string;
|
|
93
|
+
email: string;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Define your custom context
|
|
98
|
+
interface AppContext {
|
|
99
|
+
requestId: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Create router with generics
|
|
103
|
+
const router = new Router<AppState, AppContext>();
|
|
104
|
+
|
|
105
|
+
// Type-safe route handlers
|
|
106
|
+
router.get(
|
|
107
|
+
'/profile',
|
|
108
|
+
(ctx: RouterContext<AppState, AppContext>, next: Next) => {
|
|
109
|
+
// ctx.state.user is fully typed
|
|
110
|
+
if (ctx.state.user) {
|
|
111
|
+
ctx.body = {
|
|
112
|
+
user: ctx.state.user,
|
|
113
|
+
requestId: ctx.requestId // Custom context property
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Extending Types in Route Handlers
|
|
121
|
+
|
|
122
|
+
HTTP methods support generic type parameters to extend state and context types:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
interface UserState {
|
|
126
|
+
user: { id: string; name: string };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface UserContext {
|
|
130
|
+
permissions: string[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Extend types for specific routes
|
|
134
|
+
router.get<UserState, UserContext>(
|
|
135
|
+
'/users/:id',
|
|
136
|
+
async (ctx: RouterContext<UserState, UserContext>) => {
|
|
137
|
+
// ctx.state.user is fully typed
|
|
138
|
+
// ctx.permissions is fully typed
|
|
139
|
+
ctx.body = {
|
|
140
|
+
user: ctx.state.user,
|
|
141
|
+
permissions: ctx.permissions
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Parameter Middleware Types
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import type { RouterParameterMiddleware } from '@koa/router';
|
|
151
|
+
import type { Next } from 'koa';
|
|
152
|
+
|
|
153
|
+
// Type-safe parameter middleware
|
|
154
|
+
router.param('id', ((value: string, ctx: RouterContext, next: Next) => {
|
|
155
|
+
if (!/^\d+$/.test(value)) {
|
|
156
|
+
ctx.throw(400, 'Invalid ID format');
|
|
157
|
+
}
|
|
158
|
+
return next();
|
|
159
|
+
}) as RouterParameterMiddleware);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Available Types
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import {
|
|
166
|
+
Router,
|
|
167
|
+
RouterContext,
|
|
168
|
+
RouterOptions,
|
|
169
|
+
RouterMiddleware,
|
|
170
|
+
RouterParameterMiddleware,
|
|
171
|
+
RouterParamContext,
|
|
172
|
+
AllowedMethodsOptions,
|
|
173
|
+
UrlOptions,
|
|
174
|
+
HttpMethod
|
|
175
|
+
} from '@koa/router';
|
|
176
|
+
import type { Next } from 'koa';
|
|
177
|
+
|
|
178
|
+
// Router with generics
|
|
179
|
+
type MyRouter = Router<AppState, AppContext>;
|
|
180
|
+
|
|
181
|
+
// Context with generics
|
|
182
|
+
type MyContext = RouterContext<AppState, AppContext, BodyType>;
|
|
183
|
+
|
|
184
|
+
// Middleware with generics
|
|
185
|
+
type MyMiddleware = RouterMiddleware<AppState, AppContext, BodyType>;
|
|
186
|
+
|
|
187
|
+
// Parameter middleware with generics
|
|
188
|
+
type MyParamMiddleware = RouterParameterMiddleware<
|
|
189
|
+
AppState,
|
|
190
|
+
AppContext,
|
|
191
|
+
BodyType
|
|
192
|
+
>;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Type Safety Features
|
|
196
|
+
|
|
197
|
+
- ✅ **Full generic support** - `Router<StateT, ContextT>` for custom state and context types
|
|
198
|
+
- ✅ **Type-safe parameters** - `ctx.params` is fully typed
|
|
199
|
+
- ✅ **Type-safe state** - `ctx.state` respects your state type
|
|
200
|
+
- ✅ **Type-safe middleware** - Middleware functions are fully typed
|
|
201
|
+
- ✅ **Type-safe HTTP methods** - Methods support generic type extensions
|
|
202
|
+
- ✅ **Compatible with @types/koa-router** - Matches official type structure
|
|
203
|
+
|
|
204
|
+
## Quick Start
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
import Koa from 'koa';
|
|
208
|
+
import Router from '@koa/router';
|
|
209
|
+
|
|
210
|
+
const app = new Koa();
|
|
211
|
+
const router = new Router();
|
|
212
|
+
|
|
213
|
+
// Define routes
|
|
214
|
+
router.get('/', (ctx, next) => {
|
|
215
|
+
ctx.body = 'Hello World!';
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
router.get('/users/:id', (ctx, next) => {
|
|
219
|
+
ctx.body = { id: ctx.params.id };
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Apply router middleware
|
|
223
|
+
app.use(router.routes()).use(router.allowedMethods());
|
|
224
|
+
|
|
225
|
+
app.listen(3000);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## API Documentation
|
|
229
|
+
|
|
230
|
+
### Router Constructor
|
|
231
|
+
|
|
232
|
+
**`new Router([options])`**
|
|
233
|
+
|
|
234
|
+
Create a new router instance.
|
|
235
|
+
|
|
236
|
+
**Options:**
|
|
237
|
+
|
|
238
|
+
| Option | Type | Description |
|
|
239
|
+
| ----------- | ------------------------------ | ----------------------------------------- |
|
|
240
|
+
| `prefix` | `string` | Prefix all routes with this path |
|
|
241
|
+
| `exclusive` | `boolean` | Only run the most specific matching route |
|
|
242
|
+
| `host` | `string \| string[] \| RegExp` | Match routes only for this hostname(s) |
|
|
243
|
+
| `methods` | `string[]` | Custom HTTP methods to support |
|
|
244
|
+
| `sensitive` | `boolean` | Enable case-sensitive routing |
|
|
245
|
+
| `strict` | `boolean` | Require trailing slashes |
|
|
246
|
+
|
|
247
|
+
**Example:**
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
const router = new Router({
|
|
251
|
+
prefix: '/api',
|
|
252
|
+
exclusive: true,
|
|
253
|
+
host: 'example.com'
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### HTTP Methods
|
|
258
|
+
|
|
259
|
+
Router provides methods for all standard HTTP verbs:
|
|
260
|
+
|
|
261
|
+
- `router.get(path, ...middleware)`
|
|
262
|
+
- `router.post(path, ...middleware)`
|
|
263
|
+
- `router.put(path, ...middleware)`
|
|
264
|
+
- `router.patch(path, ...middleware)`
|
|
265
|
+
- `router.delete(path, ...middleware)` or `router.del(path, ...middleware)`
|
|
266
|
+
- `router.head(path, ...middleware)`
|
|
267
|
+
- `router.options(path, ...middleware)`
|
|
268
|
+
- `router.connect(path, ...middleware)` - CONNECT method
|
|
269
|
+
- `router.trace(path, ...middleware)` - TRACE method
|
|
270
|
+
- `router.all(path, ...middleware)` - Match any HTTP method
|
|
271
|
+
|
|
272
|
+
**Note:** All standard HTTP methods (as defined by Node.js `http.METHODS`) are automatically available as router methods. The `methods` option in the constructor can be used to limit which methods the router responds to, but you cannot use truly custom HTTP methods beyond the standard set.
|
|
273
|
+
|
|
274
|
+
**Basic Example:**
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
router
|
|
278
|
+
.get('/users', getUsers)
|
|
279
|
+
.post('/users', createUser)
|
|
280
|
+
.put('/users/:id', updateUser)
|
|
281
|
+
.delete('/users/:id', deleteUser)
|
|
282
|
+
.all('/users/:id', logAccess); // Runs for any method
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Using Less Common HTTP Methods:**
|
|
286
|
+
|
|
287
|
+
All standard HTTP methods from Node.js are automatically available. Here's an example using `PATCH` and `PURGE`:
|
|
288
|
+
|
|
289
|
+
```javascript
|
|
290
|
+
const router = new Router();
|
|
291
|
+
|
|
292
|
+
// PATCH method (standard HTTP method for partial updates)
|
|
293
|
+
router.patch('/users/:id', async (ctx) => {
|
|
294
|
+
// Partial update
|
|
295
|
+
ctx.body = { message: 'User partially updated' };
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// PURGE method (standard HTTP method, commonly used for cache invalidation)
|
|
299
|
+
router.purge('/cache/:key', async (ctx) => {
|
|
300
|
+
// Clear cache
|
|
301
|
+
await clearCache(ctx.params.key);
|
|
302
|
+
ctx.body = { message: 'Cache cleared' };
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// COPY method (standard HTTP method)
|
|
306
|
+
router.copy('/files/:source', async (ctx) => {
|
|
307
|
+
await copyFile(ctx.params.source, ctx.request.body.destination);
|
|
308
|
+
ctx.body = { message: 'File copied' };
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Limiting which methods the router responds to
|
|
312
|
+
const apiRouter = new Router({
|
|
313
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] // Only these methods
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
apiRouter.get('/users', getUsers);
|
|
317
|
+
apiRouter.post('/users', createUser);
|
|
318
|
+
// router.purge() won't work here because PURGE is not in the methods array
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Using Less Common HTTP Methods:**
|
|
322
|
+
|
|
323
|
+
All standard HTTP methods from Node.js are automatically available. Here's an example using `PATCH` and `PURGE`:
|
|
324
|
+
|
|
325
|
+
```javascript
|
|
326
|
+
const router = new Router();
|
|
327
|
+
|
|
328
|
+
// PATCH method (standard HTTP method for partial updates)
|
|
329
|
+
router.patch('/users/:id', async (ctx) => {
|
|
330
|
+
// Partial update
|
|
331
|
+
ctx.body = { message: 'User partially updated' };
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// PURGE method (standard HTTP method, commonly used for cache invalidation)
|
|
335
|
+
router.purge('/cache/:key', async (ctx) => {
|
|
336
|
+
// Clear cache
|
|
337
|
+
await clearCache(ctx.params.key);
|
|
338
|
+
ctx.body = { message: 'Cache cleared' };
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// COPY method (standard HTTP method)
|
|
342
|
+
router.copy('/files/:source', async (ctx) => {
|
|
343
|
+
await copyFile(ctx.params.source, ctx.request.body.destination);
|
|
344
|
+
ctx.body = { message: 'File copied' };
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Limiting which methods the router responds to
|
|
348
|
+
const apiRouter = new Router({
|
|
349
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] // Only these methods
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
apiRouter.get('/users', getUsers);
|
|
353
|
+
apiRouter.post('/users', createUser);
|
|
354
|
+
// router.purge() won't work here because PURGE is not in the methods array
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Note:** HEAD requests are automatically supported for all GET routes. When you define a GET route, HEAD requests will execute the same handler and return the same headers but with an empty body.
|
|
358
|
+
|
|
359
|
+
### Named Routes
|
|
360
|
+
|
|
361
|
+
Routes can be named for URL generation:
|
|
362
|
+
|
|
363
|
+
```javascript
|
|
364
|
+
router.get('user', '/users/:id', (ctx) => {
|
|
365
|
+
ctx.body = { id: ctx.params.id };
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Generate URL
|
|
369
|
+
router.url('user', 3);
|
|
370
|
+
// => "/users/3"
|
|
371
|
+
|
|
372
|
+
router.url('user', { id: 3 });
|
|
373
|
+
// => "/users/3"
|
|
374
|
+
|
|
375
|
+
// With query parameters
|
|
376
|
+
router.url('user', { id: 3 }, { query: { limit: 10 } });
|
|
377
|
+
// => "/users/3?limit=10"
|
|
378
|
+
|
|
379
|
+
// In middleware
|
|
380
|
+
router.use((ctx, next) => {
|
|
381
|
+
ctx.redirect(ctx.router.url('user', 1));
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Multiple Middleware
|
|
386
|
+
|
|
387
|
+
Chain multiple middleware functions for a single route:
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
router.get(
|
|
391
|
+
'/users/:id',
|
|
392
|
+
async (ctx, next) => {
|
|
393
|
+
// Load user from database
|
|
394
|
+
ctx.state.user = await User.findById(ctx.params.id);
|
|
395
|
+
return next();
|
|
396
|
+
},
|
|
397
|
+
async (ctx, next) => {
|
|
398
|
+
// Check permissions
|
|
399
|
+
if (!ctx.state.user) {
|
|
400
|
+
ctx.throw(404, 'User not found');
|
|
401
|
+
}
|
|
402
|
+
return next();
|
|
403
|
+
},
|
|
404
|
+
(ctx) => {
|
|
405
|
+
// Send response
|
|
406
|
+
ctx.body = ctx.state.user;
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Nested Routers
|
|
412
|
+
|
|
413
|
+
Mount routers within routers:
|
|
414
|
+
|
|
415
|
+
```javascript
|
|
416
|
+
const usersRouter = new Router();
|
|
417
|
+
usersRouter.get('/', getUsers);
|
|
418
|
+
usersRouter.get('/:id', getUser);
|
|
419
|
+
|
|
420
|
+
const postsRouter = new Router();
|
|
421
|
+
postsRouter.get('/', getPosts);
|
|
422
|
+
postsRouter.get('/:id', getPost);
|
|
423
|
+
|
|
424
|
+
const apiRouter = new Router({ prefix: '/api' });
|
|
425
|
+
apiRouter.use('/users', usersRouter.routes());
|
|
426
|
+
apiRouter.use('/posts', postsRouter.routes());
|
|
427
|
+
|
|
428
|
+
app.use(apiRouter.routes());
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**Note:** Parameters from parent routes are properly propagated to nested router middleware and handlers.
|
|
432
|
+
|
|
433
|
+
### Router Prefixes
|
|
434
|
+
|
|
435
|
+
Set a prefix for all routes in a router:
|
|
436
|
+
|
|
437
|
+
**Option 1: In constructor**
|
|
438
|
+
|
|
439
|
+
```javascript
|
|
440
|
+
const router = new Router({ prefix: '/api' });
|
|
441
|
+
router.get('/users', handler); // Responds to /api/users
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**Option 2: Using .prefix()**
|
|
445
|
+
|
|
446
|
+
```javascript
|
|
447
|
+
const router = new Router();
|
|
448
|
+
router.prefix('/api');
|
|
449
|
+
router.get('/users', handler); // Responds to /api/users
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**With parameters:**
|
|
453
|
+
|
|
454
|
+
```javascript
|
|
455
|
+
const router = new Router({ prefix: '/api/v:version' });
|
|
456
|
+
router.get('/users', (ctx) => {
|
|
457
|
+
ctx.body = {
|
|
458
|
+
version: ctx.params.version,
|
|
459
|
+
users: []
|
|
460
|
+
};
|
|
461
|
+
});
|
|
462
|
+
// Responds to /api/v1/users, /api/v2/users, etc.
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Note:** Middleware now correctly executes when the prefix contains parameters.
|
|
466
|
+
|
|
467
|
+
### URL Parameters
|
|
468
|
+
|
|
469
|
+
Named parameters are captured and available at `ctx.params`:
|
|
470
|
+
|
|
471
|
+
```javascript
|
|
472
|
+
router.get('/:category/:title', (ctx) => {
|
|
473
|
+
console.log(ctx.params);
|
|
474
|
+
// => { category: 'programming', title: 'how-to-node' }
|
|
475
|
+
|
|
476
|
+
ctx.body = {
|
|
477
|
+
category: ctx.params.category,
|
|
478
|
+
title: ctx.params.title
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**Optional parameters:**
|
|
484
|
+
|
|
485
|
+
```javascript
|
|
486
|
+
router.get('/user{/:id}?', (ctx) => {
|
|
487
|
+
// Matches both /user and /user/123
|
|
488
|
+
ctx.body = { id: ctx.params.id || 'all' };
|
|
489
|
+
});
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**Wildcard parameters:**
|
|
493
|
+
|
|
494
|
+
```javascript
|
|
495
|
+
router.get('/files/{/*path}', (ctx) => {
|
|
496
|
+
// Matches /files/a/b/c.txt
|
|
497
|
+
ctx.body = { path: ctx.params.path }; // => a/b/c.txt
|
|
498
|
+
});
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**Note:** Custom regex patterns in parameters (`:param(regex)`) are **no longer supported** in v14+ due to path-to-regexp v8. Use validation in handlers or middleware instead.
|
|
502
|
+
|
|
503
|
+
### router.routes()
|
|
504
|
+
|
|
505
|
+
Returns router middleware which dispatches matched routes.
|
|
506
|
+
|
|
507
|
+
```javascript
|
|
508
|
+
app.use(router.routes());
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### router.use()
|
|
512
|
+
|
|
513
|
+
Use middleware, **if and only if**, a route is matched.
|
|
514
|
+
|
|
515
|
+
**Signature:**
|
|
516
|
+
|
|
517
|
+
```javascript
|
|
518
|
+
router.use([path], ...middleware);
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**Examples:**
|
|
522
|
+
|
|
523
|
+
```javascript
|
|
524
|
+
// Run for all matched routes
|
|
525
|
+
router.use(session());
|
|
526
|
+
|
|
527
|
+
// Run only for specific path
|
|
528
|
+
router.use('/admin', requireAuth());
|
|
529
|
+
|
|
530
|
+
// Run for multiple paths
|
|
531
|
+
router.use(['/admin', '/dashboard'], requireAuth());
|
|
532
|
+
|
|
533
|
+
// Run for RegExp paths
|
|
534
|
+
router.use(/^\/api\//, apiAuth());
|
|
535
|
+
|
|
536
|
+
// Mount nested routers
|
|
537
|
+
const nestedRouter = new Router();
|
|
538
|
+
router.use('/nested', nestedRouter.routes());
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
**Note:** Middleware path boundaries are correctly enforced. Middleware scoped to `/api` will only run for routes matching `/api/*`, not for unrelated routes.
|
|
542
|
+
|
|
543
|
+
### router.prefix()
|
|
544
|
+
|
|
545
|
+
Set the path prefix for a Router instance after initialization.
|
|
546
|
+
|
|
547
|
+
```javascript
|
|
548
|
+
const router = new Router();
|
|
549
|
+
router.get('/', handler); // Responds to /
|
|
550
|
+
|
|
551
|
+
router.prefix('/api');
|
|
552
|
+
router.get('/', handler); // Now responds to /api
|
|
51
553
|
```
|
|
52
554
|
|
|
555
|
+
### router.allowedMethods()
|
|
556
|
+
|
|
557
|
+
Returns middleware for responding to `OPTIONS` requests with allowed methods,
|
|
558
|
+
and `405 Method Not Allowed` / `501 Not Implemented` responses.
|
|
559
|
+
|
|
560
|
+
**Options:**
|
|
53
561
|
|
|
54
|
-
|
|
562
|
+
| Option | Type | Description |
|
|
563
|
+
| ------------------ | ---------- | ---------------------------------------- |
|
|
564
|
+
| `throw` | `boolean` | Throw errors instead of setting response |
|
|
565
|
+
| `notImplemented` | `function` | Custom function for 501 errors |
|
|
566
|
+
| `methodNotAllowed` | `function` | Custom function for 405 errors |
|
|
55
567
|
|
|
56
|
-
|
|
57
|
-
|
|
568
|
+
**Example:**
|
|
569
|
+
|
|
570
|
+
```javascript
|
|
571
|
+
app.use(router.routes());
|
|
572
|
+
app.use(router.allowedMethods());
|
|
58
573
|
```
|
|
59
574
|
|
|
575
|
+
**With custom error handling:**
|
|
576
|
+
|
|
577
|
+
```javascript
|
|
578
|
+
app.use(
|
|
579
|
+
router.allowedMethods({
|
|
580
|
+
throw: true,
|
|
581
|
+
notImplemented: () => new Error('Not Implemented'),
|
|
582
|
+
methodNotAllowed: () => new Error('Method Not Allowed')
|
|
583
|
+
})
|
|
584
|
+
);
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### router.redirect()
|
|
588
|
+
|
|
589
|
+
Redirect `source` to `destination` URL with optional status code.
|
|
590
|
+
|
|
591
|
+
```javascript
|
|
592
|
+
router.redirect('/login', 'sign-in', 301);
|
|
593
|
+
router.redirect('/old-path', '/new-path');
|
|
594
|
+
|
|
595
|
+
// Redirect to named route
|
|
596
|
+
router.get('home', '/', handler);
|
|
597
|
+
router.redirect('/index', 'home');
|
|
598
|
+
```
|
|
60
599
|
|
|
61
|
-
|
|
600
|
+
### router.route()
|
|
62
601
|
|
|
63
|
-
|
|
602
|
+
Lookup a route by name.
|
|
603
|
+
|
|
604
|
+
```javascript
|
|
605
|
+
const layer = router.route('user');
|
|
606
|
+
if (layer) {
|
|
607
|
+
console.log(layer.path); // => /users/:id
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### router.url()
|
|
612
|
+
|
|
613
|
+
Generate URL from route name and parameters.
|
|
614
|
+
|
|
615
|
+
```javascript
|
|
616
|
+
router.get('user', '/users/:id', handler);
|
|
617
|
+
|
|
618
|
+
router.url('user', 3);
|
|
619
|
+
// => "/users/3"
|
|
620
|
+
|
|
621
|
+
router.url('user', { id: 3 });
|
|
622
|
+
// => "/users/3"
|
|
623
|
+
|
|
624
|
+
router.url('user', { id: 3 }, { query: { limit: 1 } });
|
|
625
|
+
// => "/users/3?limit=1"
|
|
626
|
+
|
|
627
|
+
router.url('user', { id: 3 }, { query: 'limit=1' });
|
|
628
|
+
// => "/users/3?limit=1"
|
|
629
|
+
```
|
|
64
630
|
|
|
631
|
+
**In middleware:**
|
|
632
|
+
|
|
633
|
+
```javascript
|
|
634
|
+
router.use((ctx, next) => {
|
|
635
|
+
// Access router instance via ctx.router
|
|
636
|
+
const userUrl = ctx.router.url('user', ctx.state.userId);
|
|
637
|
+
ctx.redirect(userUrl);
|
|
638
|
+
return next();
|
|
639
|
+
});
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### router.param()
|
|
643
|
+
|
|
644
|
+
Run middleware for named route parameters.
|
|
645
|
+
|
|
646
|
+
**Signature:**
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
router.param(param: string, middleware: RouterParameterMiddleware): Router
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
**TypeScript Example:**
|
|
653
|
+
|
|
654
|
+
```typescript
|
|
655
|
+
import type { RouterParameterMiddleware } from '@koa/router';
|
|
656
|
+
import type { Next } from 'koa';
|
|
657
|
+
|
|
658
|
+
router.param('user', (async (id: string, ctx: RouterContext, next: Next) => {
|
|
659
|
+
ctx.state.user = await User.findById(id);
|
|
660
|
+
if (!ctx.state.user) {
|
|
661
|
+
ctx.throw(404, 'User not found');
|
|
662
|
+
}
|
|
663
|
+
return next();
|
|
664
|
+
}) as RouterParameterMiddleware);
|
|
665
|
+
|
|
666
|
+
router.get('/users/:user', (ctx: RouterContext) => {
|
|
667
|
+
// ctx.state.user is already loaded and typed
|
|
668
|
+
ctx.body = ctx.state.user;
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
router.get('/users/:user/friends', (ctx: RouterContext) => {
|
|
672
|
+
// ctx.state.user is available here too
|
|
673
|
+
return ctx.state.user.getFriends();
|
|
674
|
+
});
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
**JavaScript Example:**
|
|
678
|
+
|
|
679
|
+
```javascript
|
|
680
|
+
router
|
|
681
|
+
.param('user', async (id, ctx, next) => {
|
|
682
|
+
ctx.state.user = await User.findById(id);
|
|
683
|
+
if (!ctx.state.user) {
|
|
684
|
+
ctx.throw(404, 'User not found');
|
|
685
|
+
}
|
|
686
|
+
return next();
|
|
687
|
+
})
|
|
688
|
+
.get('/users/:user', (ctx) => {
|
|
689
|
+
// ctx.state.user is already loaded
|
|
690
|
+
ctx.body = ctx.state.user;
|
|
691
|
+
})
|
|
692
|
+
.get('/users/:user/friends', (ctx) => {
|
|
693
|
+
// ctx.state.user is available here too
|
|
694
|
+
return ctx.state.user.getFriends();
|
|
695
|
+
});
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**Multiple param handlers:**
|
|
699
|
+
|
|
700
|
+
You can register multiple param handlers for the same parameter. All handlers will be called in order, and each handler is executed exactly once per request (even if multiple routes match):
|
|
701
|
+
|
|
702
|
+
```javascript
|
|
703
|
+
router
|
|
704
|
+
.param('id', validateIdFormat)
|
|
705
|
+
.param('id', checkIdExists)
|
|
706
|
+
.param('id', checkPermissions)
|
|
707
|
+
.get('/resource/:id', handler);
|
|
708
|
+
// All three param handlers run once per request
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### Router.url() (static)
|
|
712
|
+
|
|
713
|
+
Generate URL from path pattern and parameters (static method).
|
|
714
|
+
|
|
715
|
+
```javascript
|
|
716
|
+
const url = Router.url('/users/:id', { id: 1 });
|
|
717
|
+
// => "/users/1"
|
|
718
|
+
|
|
719
|
+
const url = Router.url('/users/:id', { id: 1, name: 'John' });
|
|
720
|
+
// => "/users/1"
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
## Advanced Features
|
|
724
|
+
|
|
725
|
+
### Host Matching
|
|
726
|
+
|
|
727
|
+
Match routes only for specific hostnames:
|
|
728
|
+
|
|
729
|
+
```javascript
|
|
730
|
+
// Exact match with single host
|
|
731
|
+
const routerA = new Router({
|
|
732
|
+
host: 'example.com'
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Match multiple hosts with array
|
|
736
|
+
const routerB = new Router({
|
|
737
|
+
host: ['some-domain.com', 'www.some-domain.com', 'some.other-domain.com']
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// Match patterns with RegExp
|
|
741
|
+
const routerC = new Router({
|
|
742
|
+
host: /^(.*\.)?example\.com$/ // Match all subdomains
|
|
743
|
+
});
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
**Host Matching Options:**
|
|
747
|
+
|
|
748
|
+
- `string` - Exact match (case-sensitive)
|
|
749
|
+
- `string[]` - Matches if the request host equals any string in the array
|
|
750
|
+
- `RegExp` - Pattern match using regular expression
|
|
751
|
+
- `undefined` - Matches all hosts (default)
|
|
752
|
+
|
|
753
|
+
### Regular Expressions
|
|
754
|
+
|
|
755
|
+
Use RegExp for flexible path matching:
|
|
756
|
+
|
|
757
|
+
**Full RegExp routes:**
|
|
758
|
+
|
|
759
|
+
```javascript
|
|
760
|
+
router.get(/^\/users\/(\d+)$/, (ctx) => {
|
|
761
|
+
const id = ctx.params[0]; // First capture group
|
|
762
|
+
ctx.body = { id };
|
|
763
|
+
});
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
**RegExp in router.use():**
|
|
767
|
+
|
|
768
|
+
```javascript
|
|
769
|
+
router.use(/^\/api\//, apiMiddleware);
|
|
770
|
+
router.use(/^\/admin\//, adminAuth);
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Parameter Validation
|
|
774
|
+
|
|
775
|
+
Validate parameters using middleware or handlers:
|
|
776
|
+
|
|
777
|
+
**Option 1: In Handler**
|
|
778
|
+
|
|
779
|
+
```javascript
|
|
780
|
+
router.get('/user/:id', (ctx) => {
|
|
781
|
+
if (!/^\d+$/.test(ctx.params.id)) {
|
|
782
|
+
ctx.throw(400, 'Invalid ID format');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
ctx.body = { id: parseInt(ctx.params.id, 10) };
|
|
786
|
+
});
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
**Option 2: Middleware**
|
|
790
|
+
|
|
791
|
+
```javascript
|
|
792
|
+
function validateUUID(paramName) {
|
|
793
|
+
const uuidRegex =
|
|
794
|
+
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
795
|
+
|
|
796
|
+
return async (ctx, next) => {
|
|
797
|
+
if (!uuidRegex.test(ctx.params[paramName])) {
|
|
798
|
+
ctx.throw(400, `Invalid ${paramName} format`);
|
|
799
|
+
}
|
|
800
|
+
await next();
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
router.get('/user/:id', validateUUID('id'), handler);
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
**Option 3: router.param()**
|
|
808
|
+
|
|
809
|
+
```javascript
|
|
810
|
+
router.param('id', (value, ctx, next) => {
|
|
811
|
+
if (!/^\d+$/.test(value)) {
|
|
812
|
+
ctx.throw(400, 'Invalid ID');
|
|
813
|
+
}
|
|
814
|
+
ctx.params.id = parseInt(value, 10); // Convert to number
|
|
815
|
+
return next();
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
router.get('/user/:id', handler);
|
|
819
|
+
router.get('/post/:id', handler);
|
|
820
|
+
// Both routes validate :id parameter
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
### Catch-All Routes
|
|
824
|
+
|
|
825
|
+
Create a catch-all route that only runs when no other routes match:
|
|
826
|
+
|
|
827
|
+
```javascript
|
|
828
|
+
router.get('/users', handler1);
|
|
829
|
+
router.get('/posts', handler2);
|
|
830
|
+
|
|
831
|
+
// Catch-all for unmatched routes
|
|
832
|
+
router.all('{/*rest}', (ctx) => {
|
|
833
|
+
if (!ctx.matched || ctx.matched.length === 0) {
|
|
834
|
+
ctx.status = 404;
|
|
835
|
+
ctx.body = { error: 'Not Found' };
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
### Array of Paths
|
|
841
|
+
|
|
842
|
+
Register multiple paths with the same middleware:
|
|
843
|
+
|
|
844
|
+
```javascript
|
|
845
|
+
router.get(['/users', '/people'], handler);
|
|
846
|
+
// Responds to both /users and /people
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
### 404 Handling
|
|
850
|
+
|
|
851
|
+
Implement custom 404 handling:
|
|
852
|
+
|
|
853
|
+
```javascript
|
|
854
|
+
app.use(router.routes());
|
|
855
|
+
|
|
856
|
+
// 404 middleware - runs after router
|
|
857
|
+
app.use((ctx) => {
|
|
858
|
+
if (!ctx.matched || ctx.matched.length === 0) {
|
|
859
|
+
ctx.status = 404;
|
|
860
|
+
ctx.body = {
|
|
861
|
+
error: 'Not Found',
|
|
862
|
+
path: ctx.path
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
## Best Practices
|
|
869
|
+
|
|
870
|
+
### 1. Use Middleware Composition
|
|
871
|
+
|
|
872
|
+
```javascript
|
|
873
|
+
// ✅ Good: Compose reusable middleware
|
|
874
|
+
const requireAuth = () => async (ctx, next) => {
|
|
875
|
+
if (!ctx.state.user) ctx.throw(401);
|
|
876
|
+
await next();
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
const requireAdmin = () => async (ctx, next) => {
|
|
880
|
+
if (!ctx.state.user.isAdmin) ctx.throw(403);
|
|
881
|
+
await next();
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
router.get('/admin', requireAuth(), requireAdmin(), adminHandler);
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### 2. Organize Routes by Resource
|
|
888
|
+
|
|
889
|
+
```javascript
|
|
890
|
+
// ✅ Good: Group related routes
|
|
891
|
+
const usersRouter = new Router({ prefix: '/users' });
|
|
892
|
+
usersRouter.get('/', listUsers);
|
|
893
|
+
usersRouter.post('/', createUser);
|
|
894
|
+
usersRouter.get('/:id', getUser);
|
|
895
|
+
usersRouter.put('/:id', updateUser);
|
|
896
|
+
usersRouter.delete('/:id', deleteUser);
|
|
897
|
+
|
|
898
|
+
app.use(usersRouter.routes());
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
### 3. Use Named Routes
|
|
902
|
+
|
|
903
|
+
```javascript
|
|
904
|
+
// ✅ Good: Name important routes
|
|
905
|
+
router.get('home', '/', homeHandler);
|
|
906
|
+
router.get('user-profile', '/users/:id', profileHandler);
|
|
907
|
+
|
|
908
|
+
// Easy to generate URLs
|
|
909
|
+
ctx.redirect(ctx.router.url('home'));
|
|
910
|
+
ctx.redirect(ctx.router.url('user-profile', ctx.state.user.id));
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
### 4. Validate Early
|
|
914
|
+
|
|
915
|
+
```javascript
|
|
916
|
+
// ✅ Good: Validate at the route level
|
|
917
|
+
router
|
|
918
|
+
.param('id', validateId)
|
|
919
|
+
.get('/users/:id', getUser)
|
|
920
|
+
.put('/users/:id', updateUser)
|
|
921
|
+
.delete('/users/:id', deleteUser);
|
|
922
|
+
// Validation runs once for all routes
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
### 5. Handle Errors Consistently
|
|
926
|
+
|
|
927
|
+
```javascript
|
|
928
|
+
// ✅ Good: Centralized error handling
|
|
929
|
+
app.use(async (ctx, next) => {
|
|
930
|
+
try {
|
|
931
|
+
await next();
|
|
932
|
+
} catch (err) {
|
|
933
|
+
ctx.status = err.status || 500;
|
|
934
|
+
ctx.body = {
|
|
935
|
+
error: err.message,
|
|
936
|
+
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
app.use(router.routes());
|
|
942
|
+
app.use(router.allowedMethods({ throw: true }));
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
### 6. Access Router Context Properties
|
|
946
|
+
|
|
947
|
+
The router adds useful properties to the Koa context:
|
|
948
|
+
|
|
949
|
+
```typescript
|
|
950
|
+
router.get('/users/:id', (ctx: RouterContext) => {
|
|
951
|
+
// URL parameters (fully typed)
|
|
952
|
+
const id = ctx.params.id; // string
|
|
953
|
+
|
|
954
|
+
// Router instance
|
|
955
|
+
const router = ctx.router;
|
|
956
|
+
|
|
957
|
+
// Matched route path
|
|
958
|
+
const routePath = ctx.routerPath; // => '/users/:id'
|
|
959
|
+
|
|
960
|
+
// Matched route name (if named)
|
|
961
|
+
const routeName = ctx.routerName; // => 'user' (if named)
|
|
962
|
+
|
|
963
|
+
// All matched layers
|
|
964
|
+
const matched = ctx.matched; // Array of Layer objects
|
|
965
|
+
|
|
966
|
+
// Captured values from RegExp routes
|
|
967
|
+
const captures = ctx.captures; // string[] | undefined
|
|
968
|
+
|
|
969
|
+
// Generate URLs
|
|
970
|
+
const url = ctx.router.url('user', id);
|
|
971
|
+
|
|
972
|
+
ctx.body = { id, routePath, routeName, url };
|
|
973
|
+
});
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
### 7. Type-Safe Context Extensions
|
|
977
|
+
|
|
978
|
+
Extend the router context with custom properties:
|
|
979
|
+
|
|
980
|
+
```typescript
|
|
981
|
+
import Router, { RouterContext } from '@koa/router';
|
|
982
|
+
import type { Next } from 'koa';
|
|
983
|
+
|
|
984
|
+
interface UserState {
|
|
985
|
+
user?: { id: string; email: string };
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
interface CustomContext {
|
|
989
|
+
requestId: string;
|
|
990
|
+
startTime: number;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const router = new Router<UserState, CustomContext>();
|
|
994
|
+
|
|
995
|
+
// Middleware that adds to context
|
|
996
|
+
router.use(async (ctx: RouterContext<UserState, CustomContext>, next: Next) => {
|
|
997
|
+
ctx.requestId = crypto.randomUUID();
|
|
998
|
+
ctx.startTime = Date.now();
|
|
999
|
+
await next();
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
router.get(
|
|
1003
|
+
'/users/:id',
|
|
1004
|
+
async (ctx: RouterContext<UserState, CustomContext>) => {
|
|
1005
|
+
// All properties are fully typed
|
|
1006
|
+
ctx.body = {
|
|
1007
|
+
user: ctx.state.user,
|
|
1008
|
+
requestId: ctx.requestId,
|
|
1009
|
+
duration: Date.now() - ctx.startTime
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
);
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
## Recipes
|
|
1016
|
+
|
|
1017
|
+
Common patterns and recipes for building real-world applications with @koa/router.
|
|
1018
|
+
|
|
1019
|
+
See the [recipes directory](./recipes/) for complete TypeScript examples:
|
|
1020
|
+
|
|
1021
|
+
- **[Nested Routes](./recipes/nested-routes/)** - Production-ready nested router patterns with multiple levels (3-4 levels deep), parameter propagation, and real-world examples
|
|
1022
|
+
- **[RESTful API Structure](./recipes/restful-api-structure/)** - Organize your API with nested routers
|
|
1023
|
+
- **[Authentication & Authorization](./recipes/authentication-authorization/)** - JWT-based authentication with middleware
|
|
1024
|
+
- **[Request Validation](./recipes/request-validation/)** - Validate request data with middleware
|
|
1025
|
+
- **[Parameter Validation](./recipes/parameter-validation/)** - Validate and transform parameters using router.param()
|
|
1026
|
+
- **[API Versioning](./recipes/api-versioning/)** - Implement API versioning with multiple routers
|
|
1027
|
+
- **[Error Handling](./recipes/error-handling/)** - Centralized error handling with custom error classes
|
|
1028
|
+
- **[Pagination](./recipes/pagination/)** - Implement pagination for list endpoints
|
|
1029
|
+
- **[Health Checks](./recipes/health-checks/)** - Add health check endpoints for monitoring
|
|
1030
|
+
- **[TypeScript Recipe](./recipes/typescript-recipe/)** - Full TypeScript example with types and type safety
|
|
1031
|
+
|
|
1032
|
+
Each recipe file contains complete, runnable TypeScript code that you can copy and adapt to your needs.
|
|
1033
|
+
|
|
1034
|
+
## Performance
|
|
1035
|
+
|
|
1036
|
+
@koa/router is designed for high performance:
|
|
1037
|
+
|
|
1038
|
+
- **Fast path matching** with path-to-regexp v8
|
|
1039
|
+
- **Efficient RegExp compilation** and caching
|
|
1040
|
+
- **Minimal overhead** - zero runtime type checking
|
|
1041
|
+
- **Optimized middleware execution** with koa-compose
|
|
1042
|
+
|
|
1043
|
+
**Benchmarks:**
|
|
1044
|
+
|
|
1045
|
+
```bash
|
|
1046
|
+
# Run benchmarks
|
|
1047
|
+
yarn benchmark
|
|
1048
|
+
|
|
1049
|
+
# Run all benchmark scenarios
|
|
1050
|
+
yarn benchmark:all
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
## Testing
|
|
1054
|
+
|
|
1055
|
+
@koa/router uses Node.js native test runner:
|
|
1056
|
+
|
|
1057
|
+
```bash
|
|
1058
|
+
# Run all tests (core + recipes)
|
|
1059
|
+
yarn test:all
|
|
1060
|
+
|
|
1061
|
+
# Run core tests only
|
|
1062
|
+
yarn test:core
|
|
1063
|
+
|
|
1064
|
+
# Run recipe tests only
|
|
1065
|
+
yarn test:recipes
|
|
1066
|
+
|
|
1067
|
+
# Run tests with coverage
|
|
1068
|
+
yarn test:coverage
|
|
1069
|
+
|
|
1070
|
+
# Type check
|
|
1071
|
+
yarn ts:check
|
|
1072
|
+
|
|
1073
|
+
# Format code with Prettier
|
|
1074
|
+
yarn format
|
|
1075
|
+
|
|
1076
|
+
# Check code formatting
|
|
1077
|
+
yarn format:check
|
|
1078
|
+
|
|
1079
|
+
# Lint code
|
|
1080
|
+
yarn lint
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
**Example test:**
|
|
1084
|
+
|
|
1085
|
+
```javascript
|
|
1086
|
+
import { describe, it } from 'node:test';
|
|
1087
|
+
import assert from 'node:assert';
|
|
1088
|
+
import Koa from 'koa';
|
|
1089
|
+
import Router from '@koa/router';
|
|
1090
|
+
import request from 'supertest';
|
|
1091
|
+
|
|
1092
|
+
describe('Router', () => {
|
|
1093
|
+
it('should route GET requests', async () => {
|
|
1094
|
+
const app = new Koa();
|
|
1095
|
+
const router = new Router();
|
|
1096
|
+
|
|
1097
|
+
router.get('/users', (ctx) => {
|
|
1098
|
+
ctx.body = { users: [] };
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
app.use(router.routes());
|
|
1102
|
+
|
|
1103
|
+
const res = await request(app.callback()).get('/users').expect(200);
|
|
1104
|
+
|
|
1105
|
+
assert.deepStrictEqual(res.body, { users: [] });
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
## Migration Guides
|
|
1111
|
+
|
|
1112
|
+
**Breaking Changes:**
|
|
1113
|
+
|
|
1114
|
+
- Custom regex patterns in parameters (`:param(regex)`) are **no longer supported** due to path-to-regexp v8. Use validation in handlers or middleware instead.
|
|
1115
|
+
- Node.js >= 20 is required.
|
|
1116
|
+
- TypeScript types are now included in the package (no need for `@types/@koa/router`).
|
|
1117
|
+
|
|
1118
|
+
**Upgrading:**
|
|
1119
|
+
|
|
1120
|
+
1. Update Node.js to >= 20
|
|
1121
|
+
2. Replace custom regex parameters with validation middleware
|
|
1122
|
+
3. Remove `@types/@koa/router` if installed (types are now included)
|
|
1123
|
+
4. Update any code using deprecated features
|
|
1124
|
+
|
|
1125
|
+
**Backward Compatibility:**
|
|
1126
|
+
|
|
1127
|
+
The code is mostly backward compatible. If you notice any issues when upgrading, please don't hesitate to [open an issue](https://github.com/koajs/router/issues) and let us know!
|
|
1128
|
+
|
|
1129
|
+
## Contributing
|
|
1130
|
+
|
|
1131
|
+
Contributions are welcome!
|
|
1132
|
+
|
|
1133
|
+
### Development Setup
|
|
1134
|
+
|
|
1135
|
+
```bash
|
|
1136
|
+
# Clone repository
|
|
1137
|
+
git clone https://github.com/koajs/router.git
|
|
1138
|
+
cd router
|
|
1139
|
+
|
|
1140
|
+
# Install dependencies (using yarn)
|
|
1141
|
+
yarn install
|
|
1142
|
+
|
|
1143
|
+
# Run tests
|
|
1144
|
+
yarn test:all
|
|
1145
|
+
|
|
1146
|
+
# Run tests with coverage
|
|
1147
|
+
yarn test:coverage
|
|
1148
|
+
|
|
1149
|
+
# Format code
|
|
1150
|
+
yarn format
|
|
1151
|
+
|
|
1152
|
+
# Check formatting
|
|
1153
|
+
yarn format:check
|
|
1154
|
+
|
|
1155
|
+
# Lint code
|
|
1156
|
+
yarn lint
|
|
1157
|
+
|
|
1158
|
+
# Build TypeScript
|
|
1159
|
+
yarn build
|
|
1160
|
+
|
|
1161
|
+
# Type check
|
|
1162
|
+
yarn ts:check
|
|
1163
|
+
```
|
|
65
1164
|
|
|
66
1165
|
## Contributors
|
|
67
1166
|
|
|
@@ -71,16 +1170,12 @@ See [API Reference](./API.md) for more documentation.
|
|
|
71
1170
|
| **@koajs** |
|
|
72
1171
|
| **Imed Jaberi** |
|
|
73
1172
|
|
|
74
|
-
|
|
75
1173
|
## License
|
|
76
1174
|
|
|
77
|
-
[MIT](LICENSE) ©
|
|
78
|
-
|
|
1175
|
+
[MIT](LICENSE) © Koa.js
|
|
79
1176
|
|
|
80
|
-
|
|
1177
|
+
---
|
|
81
1178
|
|
|
82
1179
|
[forward-email]: https://forwardemail.net
|
|
83
|
-
|
|
84
1180
|
[lad]: https://lad.js.org
|
|
85
|
-
|
|
86
1181
|
[npm]: https://www.npmjs.com
|