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