@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/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
  [![build status](https://github.com/koajs/router/actions/workflows/ci.yml/badge.svg)](https://github.com/koajs/router/actions/workflows/ci.yml)
6
- [![code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)
7
6
  [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
8
7
  [![made with lass](https://img.shields.io/badge/made_with-lass-95CC28.svg)](https://lass.js.org)
9
8
  [![license](https://img.shields.io/github/license/koajs/router.svg)](LICENSE)
10
9
 
11
-
12
10
  ## Table of Contents
13
11
 
14
- * [Features](#features)
15
- * [Migrating to 7 / Koa 2](#migrating-to-7--koa-2)
16
- * [Install](#install)
17
- * [Typescript Support](#typescript-support)
18
- * [API Reference](#api-reference)
19
- * [Contributors](#contributors)
20
- * [License](#license)
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
- * Express-style routing (`app.get`, `app.put`, `app.post`, etc.)
26
- * Named URL parameters
27
- * Named routes with URL generation
28
- * Match routes with specific host
29
- * Responds to `OPTIONS` requests with allowed methods
30
- * Support for `405 Method Not Allowed` and `501 Not Implemented`
31
- * Multiple route middleware
32
- * Multiple and nestable routers
33
- * `async/await` support
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
- ## Migrating to 7 / Koa 2
46
+ **npm:**
37
47
 
38
- * The API has changed to match the new promise-based middleware
39
- signature of koa 2. See the [koa 2.x readme](https://github.com/koajs/koa/tree/2.0.0-alpha.3) for more
40
- information.
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
- ## Install
54
+ ```bash
55
+ yarn add @koa/router
56
+ ```
46
57
 
47
- [npm][]:
58
+ **Requirements:**
48
59
 
49
- ```sh
50
- npm install @koa/router
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
- ## Typescript Support
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
- ```sh
57
- npm install --save-dev @types/koa__router
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
- ## API Reference
600
+ ### router.route()
62
601
 
63
- See [API Reference](./API.md) for more documentation.
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) © Alex Mingoia
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