VYPR
High severity7.3NVD Advisory· Published May 26, 2026· Updated May 26, 2026

CVE-2026-9495

CVE-2026-9495

Description

Versions of the package @koa/router from 14.0.0 and before 15.0.0 are vulnerable to Access Control Bypass due to the middleware being silently dropped from the execution chain when the router prefix contains path parameters. Depending on what the skipped middleware was supposed to protect, an attacker could bypass authentication and authorization, evade rate limiting or bypass input sanitization.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

In @koa/router versions 14.0.0 to before 15.0.0, middleware silently drops when the router prefix contains path parameters, allowing attackers to bypass authentication, authorization, or other protections.

Vulnerability

The vulnerability resides in the @koa/router package. When a router is created with a prefix that includes path parameters (e.g., /api/apps/:appId/settings), middleware registered via .use() is silently dropped from the execution chain for all routes under that router. This affects versions from 14.0.0 up to but not including 15.0.0 [2][4]. The bug was introduced in version 14.0.0 and is a regression from 13.1.1 [2].

Exploitation

An attacker needs only to send requests to routes under a router whose prefix contains path parameters. No special authentication or user interaction is required if the skipped middleware was intended to enforce such controls. The attacker simply accesses endpoints that should have been protected by the middleware (e.g., authentication, rate limiting, input sanitization), and the middleware does not execute [2]. A reproduction example from the issue shows that creating a router with prefix: '/api/apps/:appId/settings' and using .use(authorize()) results in the middleware never running, leaving ctx.state.authorized undefined [2].

Impact

An attacker can bypass any security controls implemented in the skipped middleware. This includes authentication, authorization, rate limiting, and input sanitization. The concrete impact depends on what the middleware was protecting; it could lead to unauthorized access to sensitive data, privilege escalation, or other security breaches. The vulnerability has a CVSS v3 base score of 7.3 (High) [4].

Mitigation

Upgrade @koa/router to version 15.0.0 or higher, which contains the fix. The fix was implemented in pull request #206 [3] and commit d53e17f [1]. No workaround is available for affected versions. The vulnerability is listed in the Snyk vulnerability database as SNYK-JS-KOAROUTER-12215044 [4].

AI Insight generated on May 26, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Koajs/Routerreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • (no CPE)range: >=14.0.0 <15.0.0

Patches

1
d53e17f28455

feat: re-writing in TS + fix all reported bugs + add all effective enhancments + rock to v15

https://github.com/koajs/routerImed JaberiNov 28, 2025via nvd-ref
77 files changed · +12507 2167
  • API.md+0 441 removed
    @@ -1,441 +0,0 @@
    -# API Reference
    -
    -
    -## Index
    -
    -* [Router ⏏](#router-)
    -  * [new Router(\[opts\])](#new-routeropts)
    -  * [router.get|put|post|patch|delete|del ⇒ <code>Router</code>](#routergetputpostpatchdeletedel--router)
    -  * [Named routes](#named-routes)
    -  * [Match host](#match-host)
    -  * [Multiple middleware](#multiple-middleware)
    -* [Nested routers](#nested-routers)
    -  * [Router prefixes](#router-prefixes)
    -  * [URL parameters](#url-parameters)
    -  * [router.routes ⇒ <code>function</code>](#routerroutes--function)
    -  * [router.use(\[path\], middleware) ⇒ <code>Router</code>](#routerusepath-middleware--router)
    -  * [router.prefix(prefix) ⇒ <code>Router</code>](#routerprefixprefix--router)
    -  * [router.allowedMethods(\[options\]) ⇒ <code>function</code>](#routerallowedmethodsoptions--function)
    -  * [router.redirect(source, destination, \[code\]) ⇒ <code>Router</code>](#routerredirectsource-destination-code--router)
    -  * [router.route(name) ⇒ <code>Layer</code> | <code>false</code>](#routerroutename--layer--false)
    -  * [router.url(name, params, \[options\]) ⇒ <code>String</code> | <code>Error</code>](#routerurlname-params-options--string--error)
    -  * [router.param(param, middleware) ⇒ <code>Router</code>](#routerparamparam-middleware--router)
    -  * [Router.url(path, params) ⇒ <code>String</code>](#routerurlpath-params--string)
    -
    -
    -## Router ⏏
    -
    -**Kind**: Exported class
    -
    -### new Router(\[opts])
    -
    -Create a new router.
    -
    -| Param            | Type                       | Description                                                              |
    -| ---------------- | -------------------------- | ------------------------------------------------------------------------ |
    -| [opts]           | <code>Object</code>        |                                                                          |
    -| [opts.prefix]    | <code>String</code>        | prefix router paths                                                      |
    -| [opts.exclusive] | <code>Boolean</code>       | only run last matched route's controller when there are multiple matches |
    -| [opts.host]      | <code>String/Regexp</code> | hostname to match for all routes                                         |
    -
    -**Example**
    -Basic usage:
    -
    -```javascript
    -const Koa = require('koa');
    -const Router = require('@koa/router');
    -
    -const app = new Koa();
    -const router = new Router();
    -
    -router.get('/', (ctx, next) => {
    -  // ctx.router available
    -});
    -
    -app
    -  .use(router.routes())
    -  .use(router.allowedMethods());
    -```
    -
    -### router.get|put|post|patch|delete|del ⇒ <code>Router</code>
    -
    -Create `router.verb()` methods, where *verb* is one of the HTTP verbs, such
    -as `router.get()` or `router.post()`.
    -
    -Match URL patterns to callback functions or controller actions using `router.verb()`,
    -where **verb** is one of the HTTP verbs such as `router.get()` or `router.post()`.
    -
    -Additionally, `router.all()` can be used to match against all methods.
    -
    -```javascript
    -router
    -  .get('/', (ctx, next) => {
    -    ctx.body = 'Hello World!';
    -  })
    -  .post('/users', (ctx, next) => {
    -    // ...
    -  })
    -  .put('/users/:id', (ctx, next) => {
    -    // ...
    -  })
    -  .del('/users/:id', (ctx, next) => {
    -    // ...
    -  })
    -  .all('/users/:id', (ctx, next) => {
    -    // ...
    -  });
    -```
    -
    -When a route is matched, its path is available at `ctx._matchedRoute` and if named,
    -the name is available at `ctx._matchedRouteName`
    -
    -Route paths will be translated to regular expressions using
    -[path-to-regexp](https://github.com/pillarjs/path-to-regexp).
    -
    -Query strings will not be considered when matching requests.
    -
    -### Named routes
    -
    -Routes can optionally have names. This allows generation of URLs and easy
    -renaming of URLs during development.
    -
    -```javascript
    -router.get('user', '/users/:id', (ctx, next) => {
    - // ...
    -});
    -
    -router.url('user', 3);
    -// => "/users/3"
    -```
    -
    -### Match host
    -
    -Routers can match against a specific host by using the `host` property.
    -
    -```javascript
    -const routerA = new Router({
    -  host: 'hosta.com' // only match if request host exactly equal `hosta.com`
    -});
    -
    -router.get('/', (ctx, next) => {
    -  // Response for hosta.com
    -});
    -
    -const routerB = new Router({
    -  host: /^(.*\.)?hostb\.com$/ // match all subdomains of hostb.com, including hostb.com, www.hostb.com, etc.
    -});
    -
    -router.get('/', (ctx, next) => {
    -  // Response index for matched hosts
    -});
    -```
    -
    -### Multiple middleware
    -
    -Multiple middleware may be given:
    -
    -```javascript
    -router.get(
    -  '/users/:id',
    -  (ctx, next) => {
    -    return User.findOne(ctx.params.id).then(function(user) {
    -      ctx.user = user;
    -      next();
    -    });
    -  },
    -  ctx => {
    -    console.log(ctx.user);
    -    // => { id: 17, name: "Alex" }
    -  }
    -);
    -```
    -
    -
    -## Nested routers
    -
    -Nesting routers is supported:
    -
    -```javascript
    -const forums = new Router();
    -const posts = new Router();
    -
    -posts.get('/', (ctx, next) => {...});
    -posts.get('/:pid', (ctx, next) => {...});
    -forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());
    -
    -// responds to "/forums/123/posts" and "/forums/123/posts/123"
    -app.use(forums.routes());
    -```
    -
    -### Router prefixes
    -
    -Route paths can be prefixed at the router level:
    -
    -```javascript
    -const router = new Router({
    -  prefix: '/users'
    -});
    -
    -router.get('/', ...); // responds to "/users"
    -router.get('/:id', ...); // responds to "/users/:id"
    -```
    -
    -### URL parameters
    -
    -Named route parameters are captured and added to `ctx.params`.
    -
    -```javascript
    -router.get('/:category/:title', (ctx, next) => {
    -  console.log(ctx.params);
    -  // => { category: 'programming', title: 'how-to-node' }
    -});
    -```
    -
    -The [path-to-regexp](https://github.com/pillarjs/path-to-regexp) module is
    -used to convert paths to regular expressions.
    -
    -**Kind**: instance property of <code>[Router](#exp_module_koa-router--Router)</code>
    -
    -| Param        | Type                  | Description         |
    -| ------------ | --------------------- | ------------------- |
    -| path         | <code>String</code>   |                     |
    -| [middleware] | <code>function</code> | route middleware(s) |
    -| callback     | <code>function</code> | route callback      |
    -
    -### router.routes ⇒ <code>function</code>
    -
    -Returns router middleware which dispatches a route matching the request.
    -
    -**Kind**: instance property of <code>[Router](#exp_module_koa-router--Router)</code>
    -
    -### router.use(\[path], middleware) ⇒ <code>Router</code>
    -
    -Use given middleware, **if and only if**, a route is matched.
    -
    -Middleware run in the order they are defined by `.use()`. They are invoked
    -sequentially, requests start at the first middleware and work their way
    -"down" the middleware stack.
    -
    -**Kind**: instance method of <code>[Router](#exp_module_koa-router--Router)</code>
    -
    -| Param      | Type                  |
    -| ---------- | --------------------- |
    -| [path]     | <code>String</code>   |
    -| middleware | <code>function</code> |
    -| [...]      | <code>function</code> |
    -
    -**Example**
    -
    -```javascript
    -// session middleware will run before authorize
    -router
    -  .use(session())
    -  .use(authorize());
    -
    -// use middleware only with given path
    -router.use('/users', userAuth());
    -
    -// or with an array of paths
    -router.use(['/users', '/admin'], userAuth());
    -
    -app.use(router.routes());
    -```
    -
    -### router.prefix(prefix) ⇒ <code>Router</code>
    -
    -Set the path prefix for a Router instance that was already initialized.
    -
    -**Kind**: instance method of <code>[Router](#exp_module_koa-router--Router)</code>
    -
    -| Param  | Type                |
    -| ------ | ------------------- |
    -| prefix | <code>String</code> |
    -
    -**Example**
    -
    -```javascript
    -const router = new Router({
    -  prefix: '/categories'
    -});
    -
    -router.get('/', ...); // respond "/categories"
    -
    -router.prefix('/users');
    -
    -router.get('/', ...); // responds to "/users"
    -router.get('/:id', ...); // responds to "/users/:id"
    -```
    -
    -**Note**: prefix always should start from `/` otherwise it won't work.
    -
    -### router.allowedMethods(\[options]) ⇒ <code>function</code>
    -
    -Returns separate middleware for responding to `OPTIONS` requests with
    -an `Allow` header containing the allowed methods, as well as responding
    -with `405 Method Not Allowed` and `501 Not Implemented` as appropriate.
    -
    -**Kind**: instance method of <code>[Router](#exp_module_koa-router--Router)</code>
    -
    -| Param                      | Type                  | Description                                                             |
    -| -------------------------- | --------------------- | ----------------------------------------------------------------------- |
    -| [options]                  | <code>Object</code>   |                                                                         |
    -| [options.throw]            | <code>Boolean</code>  | throw error instead of setting status and header                        |
    -| [options.notImplemented]   | <code>function</code> | throw the returned value in place of the default NotImplemented error   |
    -| [options.methodNotAllowed] | <code>function</code> | throw the returned value in place of the default MethodNotAllowed error |
    -
    -**Example**
    -
    -```javascript
    -const Koa = require('koa');
    -const Router = require('@koa/router');
    -
    -const app = new Koa();
    -const router = new Router();
    -
    -app.use(router.routes());
    -app.use(router.allowedMethods());
    -```
    -
    -**Example with [Boom](https://github.com/hapijs/boom)**
    -
    -```javascript
    -const Koa = require('koa');
    -const Router = require('@koa/router');
    -const Boom = require('@hapi/boom');
    -
    -const app = new Koa();
    -const router = new Router();
    -
    -app.use(router.routes());
    -app.use(router.allowedMethods({
    -  throw: true,
    -  notImplemented: () => Boom.notImplemented(),
    -  methodNotAllowed: () => Boom.methodNotAllowed()
    -}));
    -```
    -
    -### router.redirect(source, destination, \[code]) ⇒ <code>Router</code>
    -
    -Redirect `source` to `destination` URL with optional 30x status `code`.
    -
    -Both `source` and `destination` can be route names.
    -
    -```javascript
    -router.redirect('/login', 'sign-in');
    -```
    -
    -This is equivalent to:
    -
    -```javascript
    -router.all('/login', ctx => {
    -  ctx.redirect('/sign-in');
    -  ctx.status = 301;
    -});
    -```
    -
    -**Kind**: instance method of <code>[Router](#exp_module_koa-router--Router)</code>
    -
    -| Param       | Type                | Description                      |
    -| ----------- | ------------------- | -------------------------------- |
    -| source      | <code>String</code> | URL or route name.               |
    -| destination | <code>String</code> | URL or route name.               |
    -| [code]      | <code>Number</code> | HTTP status code (default: 301). |
    -
    -### router.route(name) ⇒ <code>Layer</code> | <code>false</code>
    -
    -Lookup route with given `name`.
    -
    -**Kind**: instance method of <code>[Router](#exp_module_koa-router--Router)</code>
    -
    -| Param | Type                |
    -| ----- | ------------------- |
    -| name  | <code>String</code> |
    -
    -### router.url(name, params, \[options]) ⇒ <code>String</code> | <code>Error</code>
    -
    -Generate URL for route. Takes a route name and map of named `params`.
    -
    -**Kind**: instance method of <code>[Router](#exp_module_koa-router--Router)</code>
    -
    -| Param           | Type                                       | Description       |
    -| --------------- | ------------------------------------------ | ----------------- |
    -| name            | <code>String</code>                        | route name        |
    -| params          | <code>Object</code>                        | url parameters    |
    -| [options]       | <code>Object</code>                        | options parameter |
    -| [options.query] | <code>Object</code> \| <code>String</code> | query options     |
    -
    -**Example**
    -
    -```javascript
    -router.get('user', '/users/:id', (ctx, next) => {
    -  // ...
    -});
    -
    -router.url('user', 3);
    -// => "/users/3"
    -
    -router.url('user', { id: 3 });
    -// => "/users/3"
    -
    -router.use((ctx, next) => {
    -  // redirect to named route
    -  ctx.redirect(ctx.router.url('sign-in'));
    -})
    -
    -router.url('user', { id: 3 }, { query: { limit: 1 } });
    -// => "/users/3?limit=1"
    -
    -router.url('user', { id: 3 }, { query: "limit=1" });
    -// => "/users/3?limit=1"
    -```
    -
    -### router.param(param, middleware) ⇒ <code>Router</code>
    -
    -Run middleware for named route parameters. Useful for auto-loading or
    -validation.
    -
    -**Kind**: instance method of <code>[Router](#exp_module_koa-router--Router)</code>
    -
    -| Param      | Type                  |
    -| ---------- | --------------------- |
    -| param      | <code>String</code>   |
    -| middleware | <code>function</code> |
    -
    -**Example**
    -
    -```javascript
    -router
    -  .param('user', (id, ctx, next) => {
    -    ctx.user = users[id];
    -    if (!ctx.user) return ctx.status = 404;
    -    return next();
    -  })
    -  .get('/users/:user', ctx => {
    -    ctx.body = ctx.user;
    -  })
    -  .get('/users/:user/friends', ctx => {
    -    return ctx.user.getFriends().then(function(friends) {
    -      ctx.body = friends;
    -    });
    -  })
    -  // /users/3 => {"id": 3, "name": "Alex"}
    -  // /users/3/friends => [{"id": 4, "name": "TJ"}]
    -```
    -
    -### Router.url(path, params) ⇒ <code>String</code>
    -
    -Generate URL from url pattern and given `params`. This method URL-encodes the parameters before including them in the URL.
    -
    -**Kind**: static method of <code>[Router](#exp_module_koa-router--Router)</code>
    -
    -| Param  | Type                | Description    |
    -| ------ | ------------------- | -------------- |
    -| path   | <code>String</code> | url pattern    |
    -| params | <code>Object</code> | url parameters |
    -
    -**Example**
    -
    -```javascript
    -const url = Router.url('/users/:id', {id: 1});
    -// => "/users/1"
    -```
    
  • bench/Makefile+0 23 removed
    @@ -1,23 +0,0 @@
    -all: middleware
    -
    -middleware:
    -	@sh ./run 1 false
    -	@sh ./run 5 false
    -	@sh ./run 10 false
    -	@sh ./run 20 false
    -	@sh ./run 50 false
    -	@sh ./run 100 false
    -	@sh ./run 200 false
    -	@sh ./run 500 false
    -	@sh ./run 1000 false
    -	@sh ./run 1 true
    -	@sh ./run 5 true
    -	@sh ./run 10 true
    -	@sh ./run 20 true
    -	@sh ./run 50 true
    -	@sh ./run 100 true
    -	@sh ./run 200 true
    -	@sh ./run 500 true
    -	@sh ./run 1000 true
    -
    -.PHONY: all middleware
    
  • bench/make.ts+83 0 added
    @@ -0,0 +1,83 @@
    +/**
    + * Benchmark Makefile equivalent in TypeScript
    + *
    + * Runs all benchmark tests with different factors and middleware configurations.
    + * This replaces the Makefile for cross-platform compatibility.
    + */
    +
    +import { spawn } from 'node:child_process';
    +import { join } from 'node:path';
    +
    +const projectRoot = join(
    +  typeof __dirname !== 'undefined' ? __dirname : process.cwd(),
    +  '..'
    +);
    +
    +const factors = [1, 5, 10, 20, 50, 100, 200, 500, 1000];
    +const middlewareOptions = [false, true];
    +
    +async function runBenchmark(
    +  factor: number,
    +  useMiddleware: boolean
    +): Promise<void> {
    +  return new Promise((resolve, reject) => {
    +    const childProcess = spawn(
    +      'node',
    +      [
    +        '--require',
    +        'ts-node/register',
    +        'bench/run.ts',
    +        String(factor),
    +        String(useMiddleware)
    +      ],
    +      {
    +        env: {
    +          ...process.env,
    +          TS_NODE_PROJECT: 'tsconfig.bench.json'
    +        },
    +        stdio: 'inherit',
    +        cwd: projectRoot
    +      }
    +    );
    +
    +    childProcess.on('close', (code) => {
    +      if (code === 0) {
    +        resolve();
    +      } else {
    +        reject(new Error(`Benchmark failed with code ${code}`));
    +      }
    +    });
    +
    +    childProcess.on('error', (error) => {
    +      reject(error);
    +    });
    +  });
    +}
    +
    +async function runAllBenchmarks(): Promise<void> {
    +  console.log('Running all benchmarks...\n');
    +
    +  for (const useMiddleware of middlewareOptions) {
    +    console.log(`\nMiddleware: ${useMiddleware}\n`);
    +
    +    for (const factor of factors) {
    +      try {
    +        await runBenchmark(factor, useMiddleware);
    +        await new Promise((resolve) => setTimeout(resolve, 100));
    +      } catch (error) {
    +        console.error(
    +          `Error running benchmark with factor ${factor}, middleware ${useMiddleware}:`,
    +          error
    +        );
    +        process.exit(1);
    +      }
    +    }
    +  }
    +
    +  console.log('\nAll benchmarks completed!');
    +}
    +
    +runAllBenchmarks().catch((error) => {
    +  console.error('Fatal error:', error);
    +  process.exit(1);
    +});
    
  • bench/REQUIREMENTS.md+89 0 added
    @@ -0,0 +1,89 @@
    +# Benchmark Requirements
    +
    +This folder contains benchmark scripts that require the `wrk` HTTP benchmarking tool.
    +
    +## Installation Instructions
    +
    +### macOS
    +
    +```bash
    +brew install wrk
    +```
    +
    +### Linux
    +
    +```bash
    +sudo apt-get install wrk
    +# or use your package manager (yum, dnf, pacman, etc.)
    +```
    +
    +### Windows
    +
    +`wrk` does not natively support Windows. You have the following options:
    +
    +#### Option 1: Use WSL (Windows Subsystem for Linux) - Recommended
    +
    +1. Install WSL:
    +
    +   ```powershell
    +   wsl --install
    +   ```
    +
    +2. Open WSL terminal and install wrk:
    +
    +   ```bash
    +   sudo apt-get update
    +   sudo apt-get install wrk
    +   ```
    +
    +3. Run benchmarks from WSL or set `WRK_PATH` environment variable:
    +   ```powershell
    +   set WRK_PATH=wsl wrk
    +   ```
    +
    +#### Option 2: Use Alternative Tools
    +
    +**autocannon** (Node.js-based, cross-platform):
    +
    +```bash
    +npm install -g autocannon
    +```
    +
    +Then set `WRK_PATH`:
    +
    +```powershell
    +set WRK_PATH=autocannon
    +```
    +
    +**Apache Bench (ab)**:
    +
    +- Install Apache HTTP Server which includes `ab` tool
    +- Set `WRK_PATH` to point to the `ab` executable
    +
    +## Environment Variables
    +
    +You can customize the benchmark tool using environment variables:
    +
    +- `WRK_PATH`: Path to the wrk executable (default: `wrk`)
    +- `PORT`: Server port for benchmarks (default: `3000`)
    +
    +Example:
    +
    +```bash
    +export WRK_PATH=/usr/local/bin/wrk
    +export PORT=3000
    +npm run bench 10 false
    +```
    +
    +## Usage
    +
    +After installing `wrk`, you can run benchmarks:
    +
    +```bash
    +# Single benchmark
    +npm run bench <factor> <useMiddleware>
    +npm run bench 10 false
    +
    +# Run all benchmarks
    +npm run bench:all
    +```
    
  • bench/run+0 31 removed
    @@ -1,31 +0,0 @@
    -#!/bin/env bash
    -
    -set -e
    -set -o allexport; source "$(dirname $0)/../.env"; set +o allexport
    -export FACTOR=$1
    -export USE_MIDDLEWARE=$2
    -
    -host="http://localhost:$PORT"
    -
    -node "$(dirname $0)/server.js" &
    -
    -pid=$!
    -
    -curl \
    -  --retry-connrefused \
    -  --retry 5 \
    -  --retry-delay 0 \
    -  -s \
    -  "$host/_health" \
    -  > /dev/null
    -
    -# siege -c 50 -t 8 "$host/10/child/grandchild/%40"
    -wrk "$host/10/child/grandchild/%40" \
    -  -d 3 \
    -  -c 50 \
    -  -t 8 \
    -  | grep 'Requests/sec' \
    -  | awk '{ print "  " $2 }'
    -
    -kill $pid 
    -exit
    \ No newline at end of file
    
  • bench/run-bench.ts+24 14 renamed
    @@ -1,11 +1,20 @@
    -'use strict';
    +/**
    + * Benchmark runner - runs router.match() benchmarks
    + *
    + * This script benchmarks the router matching performance.
    + */
     
    -const KoaRouter = require('../lib/router');
    -const { now, print, operations } = require('./util');
    +import Router from '../src';
    +import { now, print } from './util';
     
    -const router = new KoaRouter();
    +const router = new Router();
     
    -const routes = [
    +interface Route {
    +  method: string;
    +  url: string;
    +}
    +
    +const routes: Route[] = [
       { method: 'GET', url: '/user' },
       { method: 'GET', url: '/user/comments' },
       { method: 'GET', url: '/user/avatar' },
    @@ -20,7 +29,7 @@ const routes = [
       { method: 'GET', url: '/static/{/*path}' }
     ];
     
    -function noop() {}
    +function noop(): void {}
     
     let i = 0;
     let time = 0;
    @@ -34,49 +43,49 @@ for (const route of routes) {
     }
     
     time = now();
    -for (i = 0; i < operations; i++) {
    +for (i = 0; i < 1_000_000; i++) {
       router.match('/user', 'GET');
     }
     
     print('short static:', time);
     
     time = now();
    -for (i = 0; i < operations; i++) {
    +for (i = 0; i < 1_000_000; i++) {
       router.match('/user/comments', 'GET');
     }
     
     print('static with same radix:', time);
     
     time = now();
    -for (i = 0; i < operations; i++) {
    +for (i = 0; i < 1_000_000; i++) {
       router.match('/user/lookup/username/john', 'GET');
     }
     
     print('dynamic route:', time);
     
     time = now();
    -for (i = 0; i < operations; i++) {
    +for (i = 0; i < 1_000_000; i++) {
       router.match('/event/abcd1234/comments', 'GET');
     }
     
     print('mixed static dynamic:', time);
     
     time = now();
    -for (i = 0; i < operations; i++) {
    +for (i = 0; i < 1_000_000; i++) {
       router.match('/very/deeply/nested/route/hello/there', 'GET');
     }
     
     print('long static:', time);
     
     time = now();
    -for (i = 0; i < operations; i++) {
    +for (i = 0; i < 1_000_000; i++) {
       router.match('/static/index.html', 'GET');
     }
     
     print('wildcard:', time);
     
     time = now();
    -for (i = 0; i < operations; i++) {
    +for (i = 0; i < 1_000_000; i++) {
       router.match('/user', 'GET');
       router.match('/user/comments', 'GET');
       router.match('/user/lookup/username/john', 'GET');
    @@ -87,4 +96,5 @@ for (i = 0; i < operations; i++) {
     
     const output = print('all together:', time);
     
    -require('fs').writeFileSync('bench-result.txt', String(output));
    +import { writeFileSync } from 'node:fs';
    +writeFileSync('bench-result.txt', String(output));
    
  • bench/run.ts+153 0 added
    @@ -0,0 +1,153 @@
    +/**
    + * Benchmark runner script
    + *
    + * Runs a single benchmark test with specified factor and middleware usage.
    + * Usage: node --require ts-node/register bench/run.ts <factor> <useMiddleware>
    + * Example: node --require ts-node/register bench/run.ts 10 false
    + */
    +
    +import { spawn } from 'node:child_process';
    +import { join } from 'node:path';
    +
    +const projectRoot = join(
    +  typeof __dirname !== 'undefined' ? __dirname : process.cwd(),
    +  '..'
    +);
    +
    +const factor = process.argv[2] || '10';
    +const useMiddleware = process.argv[3] === 'true';
    +const port = process.env.PORT || '3000';
    +const host = `http://localhost:${port}`;
    +
    +const serverProcess = spawn(
    +  'node',
    +  ['--require', 'ts-node/register', 'bench/server.ts'],
    +  {
    +    env: {
    +      ...process.env,
    +      TS_NODE_PROJECT: 'tsconfig.bench.json',
    +      FACTOR: factor,
    +      USE_MIDDLEWARE: String(useMiddleware),
    +      PORT: port
    +    },
    +    stdio: 'pipe',
    +    cwd: projectRoot
    +  }
    +);
    +
    +let serverOutput = '';
    +serverProcess.stdout?.on('data', (data) => {
    +  serverOutput += data.toString();
    +});
    +
    +serverProcess.stderr?.on('data', (data) => {
    +  console.error(data.toString());
    +});
    +
    +async function waitForServer(maxRetries = 30, delay = 200): Promise<void> {
    +  for (let i = 0; i < maxRetries; i++) {
    +    try {
    +      const response = await fetch(`${host}/_health`);
    +      if (response.ok) {
    +        return;
    +      }
    +    } catch {}
    +    await new Promise((resolve) => setTimeout(resolve, delay));
    +  }
    +  throw new Error('Server failed to start');
    +}
    +
    +async function runBenchmark(): Promise<void> {
    +  try {
    +    await waitForServer();
    +
    +    const wrkPath = process.env.WRK_PATH || 'wrk';
    +
    +    const { execSync } = await import('node:child_process');
    +    const isWindows = process.platform === 'win32';
    +
    +    try {
    +      if (isWindows) {
    +        try {
    +          execSync(`where ${wrkPath}`, { stdio: 'ignore' });
    +        } catch {
    +          try {
    +            execSync(`wsl which ${wrkPath}`, { stdio: 'ignore' });
    +          } catch {
    +            throw new Error('wrk not found');
    +          }
    +        }
    +      } else {
    +        execSync(`which ${wrkPath}`, { stdio: 'ignore' });
    +      }
    +    } catch {
    +      console.error(`\nError: '${wrkPath}' command not found.`);
    +      console.error('Please install wrk:');
    +      if (isWindows) {
    +        console.error('  Windows options:');
    +        console.error('    1. Use WSL (Windows Subsystem for Linux):');
    +        console.error('       - Install WSL: wsl --install');
    +        console.error('       - Then in WSL: sudo apt-get install wrk');
    +        console.error('    2. Use alternative tools that work on Windows:');
    +        console.error('       - autocannon: npm install -g autocannon');
    +        console.error(
    +          '       - Apache Bench (ab): Install via Apache HTTP Server'
    +        );
    +        console.error(
    +          '    3. Set WRK_PATH to point to wrk executable in WSL or alternative tool'
    +        );
    +      } else {
    +        console.error('  macOS: brew install wrk');
    +        console.error(
    +          '  Linux: sudo apt-get install wrk (or use your package manager)'
    +        );
    +      }
    +      console.error(
    +        '  Or set WRK_PATH environment variable to point to wrk executable\n'
    +      );
    +      process.exit(1);
    +    }
    +
    +    const wrkProcess = spawn(
    +      wrkPath,
    +      [`${host}/10/child/grandchild/%40`, '-d', '3', '-c', '50', '-t', '8'],
    +      {
    +        stdio: 'pipe'
    +      }
    +    );
    +
    +    let wrkOutput = '';
    +    wrkProcess.stdout?.on('data', (data) => {
    +      wrkOutput += data.toString();
    +    });
    +
    +    wrkProcess.stderr?.on('data', () => {});
    +
    +    await new Promise<void>((resolve, reject) => {
    +      wrkProcess.on('close', (code) => {
    +        if (code === 0) {
    +          const match = wrkOutput.match(/Requests\/sec:\s+([\d.]+)/);
    +          if (match) {
    +            console.log(`  ${match[1]}`);
    +          } else {
    +            console.log('  Unable to parse requests/sec');
    +          }
    +          resolve();
    +        } else {
    +          reject(new Error(`wrk exited with code ${code}`));
    +        }
    +      });
    +    });
    +  } catch (error) {
    +    console.error('Error running benchmark:', error);
    +    process.exit(1);
    +  } finally {
    +    serverProcess.kill();
    +    await new Promise((resolve) => setTimeout(resolve, 100));
    +  }
    +}
    +
    +runBenchmark().catch((error) => {
    +  console.error('Fatal error:', error);
    +  process.exit(1);
    +});
    
  • bench/server.js+0 57 removed
    @@ -1,57 +0,0 @@
    -const process = require('node:process');
    -const env = require('@ladjs/env')({
    -  path: '../.env',
    -  includeProcessEnv: true,
    -  assignToProcessEnv: true
    -});
    -const Koa = require('koa');
    -
    -const Router = require('../');
    -
    -const app = new Koa();
    -const router = new Router();
    -
    -const ok = (ctx) => {
    -  ctx.status = 200;
    -};
    -
    -const n = Number.parseInt(env.FACTOR || '10', 10);
    -const useMiddleware = env.USE_MIDDLEWARE === 'true';
    -
    -router.get('/_health', ok);
    -
    -for (let i = n; i > 0; i--) {
    -  if (useMiddleware) router.use((ctx, next) => next());
    -  router.get(`/${i}/one`, ok);
    -  router.get(`/${i}/one/two`, ok);
    -  router.get(`/${i}/one/two/:three`, ok);
    -  router.get(`/${i}/one/two/:three/:four?`, ok);
    -  router.get(`/${i}/one/two/:three/:four?/five`, ok);
    -  router.get(`/${i}/one/two/:three/:four?/five/six`, ok);
    -}
    -
    -const grandchild = new Router();
    -
    -if (useMiddleware) grandchild.use((ctx, next) => next());
    -grandchild.get('/', ok);
    -grandchild.get('/:id', ok);
    -grandchild.get('/:id/seven', ok);
    -grandchild.get('/:id/seven(/eight)?', ok);
    -
    -for (let i = n; i > 0; i--) {
    -  const child = new Router();
    -  if (useMiddleware) child.use((ctx, next) => next());
    -  child.get(`/:${''.padStart(i, 'a')}`, ok);
    -  child.middleware('/grandchild', grandchild);
    -  router.middleware(`/${i}/child`, child);
    -}
    -
    -if (process.env.DEBUG) {
    -  console.log(require('../lib/utils').inspect(router));
    -}
    -
    -app.use(router.routes());
    -
    -process.stdout.write(`mw: ${useMiddleware} factor: ${n} requests/sec`);
    -
    -app.listen(env.PORT);
    
  • bench/server.ts+64 0 added
    @@ -0,0 +1,64 @@
    +/**
    + * Benchmark server
    + *
    + * Creates a Koa server with routes for benchmarking.
    + * Configured via environment variables:
    + * - FACTOR: Number of routes to create (default: 10)
    + * - USE_MIDDLEWARE: Whether to use middleware (default: false)
    + * - PORT: Server port (default: 3000)
    + */
    +
    +import process from 'node:process';
    +import Koa from 'koa';
    +import Router from '../src';
    +
    +const app = new Koa();
    +const router = new Router();
    +
    +const ok = (ctx: any): void => {
    +  ctx.status = 200;
    +};
    +
    +const n = Number.parseInt(process.env.FACTOR || '10', 10);
    +const useMiddleware = process.env.USE_MIDDLEWARE === 'true';
    +
    +router.get('/_health', ok);
    +
    +for (let i = n; i > 0; i--) {
    +  if (useMiddleware) router.use((_: any, next: any) => next());
    +  router.get(`/${i}/one`, ok);
    +  router.get(`/${i}/one/two`, ok);
    +  router.get(`/${i}/one/two/:three`, ok);
    +  router.get(`/${i}/one/two/:three/:four`, ok);
    +  router.get(`/${i}/one/two/:three/:four/five`, ok);
    +  router.get(`/${i}/one/two/:three/:four/five/six`, ok);
    +}
    +
    +const grandchild = new Router();
    +
    +if (useMiddleware) grandchild.use((_: any, next: any) => next());
    +grandchild.get('/', ok);
    +grandchild.get('/:id', ok);
    +grandchild.get('/:id/seven', ok);
    +grandchild.get('/:id/seven', ok);
    +grandchild.get('/:id/seven/eight', ok);
    +
    +for (let i = n; i > 0; i--) {
    +  const child = new Router();
    +  if (useMiddleware) child.use((_: any, next: any) => next());
    +  child.get(`/:${''.padStart(i, 'a')}`, ok);
    +  child.use('/grandchild', grandchild.routes(), grandchild.allowedMethods());
    +  router.use(`/${i}/child`, child.routes(), child.allowedMethods());
    +}
    +
    +if (process.env.DEBUG) {
    +  // eslint-disable-next-line no-console
    +  console.log('Router debug info:', router);
    +}
    +
    +app.use(router.routes());
    +
    +process.stdout.write(`mw: ${useMiddleware} factor: ${n} requests/sec`);
    +
    +const port = Number.parseInt(process.env.PORT || '3000', 10);
    +app.listen(port);
    
  • bench/util.js+0 55 removed
    @@ -1,55 +0,0 @@
    -'use strict';
    -
    -const process = require('node:process');
    -
    -const chalk = require('chalk').default;
    -
    -const operations = 1000000;
    -
    -function now() {
    -  const ts = process.hrtime();
    -  return ts[0] * 1e3 + ts[1] / 1e6;
    -}
    -
    -function getOpsSec(ms) {
    -  return Number(((operations * 1000) / ms).toFixed(0));
    -}
    -
    -function print(name, time) {
    -  const opsSec = getOpsSec(now() - time);
    -  console.log(chalk.yellow(name), opsSec.toLocaleString(), 'ops/sec');
    -  return Number(opsSec);
    -}
    -
    -function title(name) {
    -  console.log(
    -    chalk.green(`
    -${'='.repeat(name.length + 2)}
    - ${name}
    -${'='.repeat(name.length + 2)}`)
    -  );
    -}
    -
    -function Queue() {
    -  this.q = [];
    -  this.running = false;
    -}
    -
    -Queue.prototype.add = function add(job) {
    -  this.q.push(job);
    -  if (!this.running) this.run();
    -};
    -
    -Queue.prototype.run = function run() {
    -  this.running = true;
    -  const job = this.q.shift();
    -  job(() => {
    -    if (this.q.length > 0) {
    -      this.run();
    -    } else {
    -      this.running = false;
    -    }
    -  });
    -};
    -
    -module.exports = { now, getOpsSec, print, title, Queue, operations };
    
  • bench/util.ts+52 0 added
    @@ -0,0 +1,52 @@
    +import process from 'node:process';
    +import chalk from 'chalk';
    +
    +export const operations = 1_000_000;
    +
    +export function now(): number {
    +  const ts = process.hrtime();
    +  return ts[0] * 1e3 + ts[1] / 1e6;
    +}
    +
    +export function getOpsSec(ms: number): number {
    +  return Number(((operations * 1000) / ms).toFixed(0));
    +}
    +
    +export function print(name: string, time: number): number {
    +  const opsSec = getOpsSec(now() - time);
    +  console.log(chalk.yellow(name), opsSec.toLocaleString(), 'ops/sec');
    +  return Number(opsSec);
    +}
    +
    +export function title(name: string): void {
    +  console.log(
    +    chalk.green(`
    +${'='.repeat(name.length + 2)}
    + ${name}
    +${'='.repeat(name.length + 2)}`)
    +  );
    +}
    +
    +export class Queue {
    +  private q: Array<(callback: () => void) => void> = [];
    +  private running = false;
    +
    +  add(job: (callback: () => void) => void): void {
    +    this.q.push(job);
    +    if (!this.running) this.run();
    +  }
    +
    +  private run(): void {
    +    this.running = true;
    +    const job = this.q.shift();
    +    if (job) {
    +      job(() => {
    +        if (this.q.length > 0) {
    +          this.run();
    +        } else {
    +          this.running = false;
    +        }
    +      });
    +    }
    +  }
    +}
    
  • .commitlintrc.js+0 3 removed
    @@ -1,3 +0,0 @@
    -module.exports = {
    -  extends: ['@commitlint/config-conventional']
    -};
    
  • .commitlintrc.json+3 0 added
    @@ -0,0 +1,3 @@
    +{
    +  "extends": ["@commitlint/config-conventional"]
    +}
    
  • .editorconfig+0 9 removed
    @@ -1,9 +0,0 @@
    -root = true
    -
    -[*]
    -indent_style = space
    -indent_size = 2
    -end_of_line = lf
    -charset = utf-8
    -trim_trailing_whitespace = true
    -insert_final_newline = true
    
  • eslint.config.js+34 23 modified
    @@ -1,31 +1,42 @@
     const js = require('@eslint/js');
    -const eslintPluginUnicorn = require('eslint-plugin-unicorn');
    +const unicorn = require('eslint-plugin-unicorn');
    +const tsPlugin = require('@typescript-eslint/eslint-plugin');
    +const tsParser = require('@typescript-eslint/parser');
    +
    +const unicornPlugin = unicorn.default || unicorn;
     
     module.exports = [
    -  js.configs.recommended,
       {
    +    ignores: [
    +      'node_modules/**',
    +      'coverage/**',
    +      'dist/**',
    +      'bench/**',
    +      'examples/**',
    +      'recipes/*.ts'
    +    ]
    +  },
    +  // JavaScript files
    +  {
    +    files: ['**/*.{js,cjs,mjs}'],
    +    ...js.configs.recommended,
    +    plugins: { unicorn: unicornPlugin },
    +    rules: unicornPlugin.configs.recommended.rules
    +  },
    +  // TypeScript source files
    +  {
    +    files: ['src/**/*.ts'],
         languageOptions: {
    -      ecmaVersion: 2022,
    -      sourceType: 'commonjs',
    -      globals: {
    -        console: true,
    -        setTimeout: true,
    -        __dirname: true,
    -        // Mocha globals
    -        before: true,
    -        after: true,
    -        beforeEach: true,
    -        describe: true,
    -        it: true
    -      }
    +      parser: tsParser,
    +      parserOptions: { project: './tsconfig.json' }
         },
    -    plugins: {
    -      unicorn: eslintPluginUnicorn,
    -    },
    -    rules: {
    -      'promise/prefer-await-to-then': 0,
    -      'logical-assignment-operators': 0,
    -      'arrow-body-style': 0,
    -    }
    +    plugins: { '@typescript-eslint': tsPlugin, unicorn: unicornPlugin },
    +    rules: unicornPlugin.configs.recommended.rules
    +  },
    +  // TypeScript test files (relaxed)
    +  {
    +    files: ['test/**/*.ts', 'recipes/**/*.test.ts', '*.config.ts'],
    +    languageOptions: { parser: tsParser },
    +    plugins: { '@typescript-eslint': tsPlugin }
       }
     ];
    
  • .github/workflows/ci.yml+60 21 modified
    @@ -10,40 +10,76 @@ jobs:
             os:
               - ubuntu-latest
             node_version:
    -          - 20
    -          - 22
    -          - 24
    +          - '20'
    +          - '22'
    +          - '24'
    +          - '25'
         name: Node ${{ matrix.node_version }} on ${{ matrix.os }}
         steps:
    -      - uses: actions/checkout@v3
    +      - uses: actions/checkout@v4
           - name: Setup node
    -        uses: actions/setup-node@v3
    +        uses: actions/setup-node@v4
             with:
               node-version: ${{ matrix.node_version }}
    +          cache: 'yarn'
           - name: Install dependencies
    -        run: npm install
    -      - name: Run tests
    -        run: npm run test
    +        run: yarn install --frozen-lockfile
    +      - name: Run lint
    +        run: yarn run lint
    +      - name: Run test coverage
    +        run: yarn run test:coverage
     
       benchmarks:
         runs-on: ubuntu-latest
         env:
           THRESHOLD: 50000
         steps:
    -      - name: Setup node
    -        uses: actions/setup-node@v3
    -        with:
    -          node-version: 20
    +      - name: Install wrk
    +        run: |
    +          sudo apt-get update
    +          sudo apt-get install -y wrk
    +      - name: Verify wrk installation
    +        run: |
    +          if ! command -v wrk &> /dev/null; then
    +            echo "Error: wrk installation failed"
    +            exit 1
    +          fi
    +          echo "wrk installed successfully: $(wrk --version 2>&1 | head -1)"
           # First checkout master and run benchmarks
           - name: Checkout master branch
    -        uses: actions/checkout@v3
    +        uses: actions/checkout@v4
             with:
               ref: master
    -
    +      - name: Check if yarn.lock exists
    +        id: check-yarn-lock
    +        run: |
    +          if [ -f yarn.lock ]; then
    +            echo "exists=true" >> $GITHUB_OUTPUT
    +            echo "yarn.lock found: $(wc -l < yarn.lock) lines"
    +          else
    +            echo "exists=false" >> $GITHUB_OUTPUT
    +            echo "yarn.lock not found on master branch"
    +          fi
    +      - name: Setup node
    +        if: steps.check-yarn-lock.outputs.exists == 'true'
    +        uses: actions/setup-node@v4
    +        with:
    +          node-version: '24'
    +          cache: 'yarn'
    +      - name: Setup node without cache
    +        if: steps.check-yarn-lock.outputs.exists == 'false'
    +        uses: actions/setup-node@v4
    +        with:
    +          node-version: '24'
           - name: Install dependencies
    -        run: npm install
    +        run: |
    +          if [ -f yarn.lock ]; then
    +            yarn install --frozen-lockfile
    +          else
    +            yarn install
    +          fi
           - name: Run benchmarks
    -        run: npm run benchmark || echo "0" > bench-result.txt
    +        run: yarn run benchmark || echo "0" > bench-result.txt
           # Store output of bench-result.txt to workflow output
           - name: Store benchmark result
             id: main_benchmark
    @@ -52,12 +88,16 @@ jobs:
     
           # Now checkout the PR branch and run benchmarks
           - name: Checkout PR branch
    -        uses: actions/checkout@v3
    -
    +        uses: actions/checkout@v4
    +      - name: Setup node for PR branch
    +        uses: actions/setup-node@v4
    +        with:
    +          node-version: '24'
    +          cache: 'yarn'
           - name: Install dependencies
    -        run: npm install
    +        run: yarn install --frozen-lockfile
           - name: Run benchmarks
    -        run: npm run benchmark
    +        run: yarn run benchmark
           # Store output of bench-result.txt to workflow output
           - name: Store benchmark result
             id: branch_benchmark
    @@ -79,4 +119,3 @@ jobs:
               else
                 echo "Benchmark difference is within acceptable limit (< $THRESHOLD)."
               fi
    -          
    
  • .gitignore+19 10 modified
    @@ -1,16 +1,25 @@
    +#       OS        #
    +###################
     .DS_Store
    -*.log
     .idea
    -node_modules
    -coverage
    -.nyc_output
    -locales/
    -package-lock.json
    -yarn.lock
    -
     Thumbs.db
     tmp/
     temp/
    +bench-result.txt
    +docs/
    +
    +#     Node.js     #
    +###################
    +node_modules
    +
    +
    +#      Build      #
    +###################
    +dist
    +build
    +
    +#       NYC       #
    +###################
    +coverage
     *.lcov
    -.env
    -bench-result.txt
    \ No newline at end of file
    +.nyc_output
    
  • HISTORY.md+0 187 removed
    @@ -1,187 +0,0 @@
    -# History
    -
    -**[History has moved to the Releases tab of GitHub](https://github.com/koajs/router/releases).**
    -
    -
    -## Log
    -
    -
    -## 9.0.0 / 2020-04-09
    -
    -* Update `path-to-regexp`. Migration path: change usage of `'*'` in routes to `(.*)` or `:splat*`.
    -  * Example: `router.get('*', ....)` becomes `router.get('(.*)') ....)`
    -
    -
    -## 8.0.0 / 2019-06-16
    -
    -**others**
    -
    -* [b5dd5e8](http://github.com/koajs/koa-router/commit/b5dd5e8f00e841b7061a62ab6228cbe96a999470)] - chore: rename to @koa/router (dead-horse)
    -
    ----
    -
    -Changelogs inherit from koa-router.
    -
    -
    -## 7.4.0
    -
    -* Fix router.url() for multiple nested routers [#407](https://github.com/alexmingoia/koa-router/pull/407)
    -* `layer.name` added to `ctx` at `ctx.routerName` during routing [#412](https://github.com/alexmingoia/koa-router/pull/412)
    -* Router.use() was erroneously settings `(.*)` as a prefix to all routers nested with .use that did not pass an explicit prefix string as the first argument. This resulted in routes being matched that should not have been, included the running of multiple route handlers in error. [#369](https://github.com/alexmingoia/koa-router/issues/369) and [#410](https://github.com/alexmingoia/koa-router/issues/410) include information on this issue.
    -
    -
    -## 7.3.0
    -
    -* Router#url() now accepts query parameters to add to generated urls [#396](https://github.com/alexmingoia/koa-router/pull/396)
    -
    -
    -## 7.2.1
    -
    -* Respond to CORS preflights with 200, 0 length body [#359](https://github.com/alexmingoia/koa-router/issues/359)
    -
    -
    -## 7.2.0
    -
    -* Fix a bug in Router#url and append Router object to ctx. [#350](https://github.com/alexmingoia/koa-router/pull/350)
    -* Adds `_matchedRouteName` to context [#337](https://github.com/alexmingoia/koa-router/pull/337)
    -* Respond to CORS preflights with 200, 0 length body [#359](https://github.com/alexmingoia/koa-router/issues/359)
    -
    -
    -## 7.1.1
    -
    -* Fix bug where param handlers were run out of order [#282](https://github.com/alexmingoia/koa-router/pull/282)
    -
    -
    -## 7.1.0
    -
    -* Backports: merge 5.4 work into the 7.x upstream. See 5.4.0 updates for more details.
    -
    -
    -## 7.0.1
    -
    -* Fix: allowedMethods should be ctx.method not this.method [#215](https://github.com/alexmingoia/koa-router/pull/215)
    -
    -
    -## 7.0.0
    -
    -* The API has changed to match the new promise-based middleware
    -  signature of koa 2. See the
    -  [koa 2.x readme](https://github.com/koajs/koa/tree/2.0.0-alpha.3) for more
    -  information.
    -* Middleware is now always run in the order declared by `.use()` (or `.get()`,
    -  etc.), which matches Express 4 API.
    -
    -
    -## 5.4.0
    -
    -* Expose matched route at `ctx._matchedRoute`.
    -
    -
    -## 5.3.0
    -
    -* Register multiple routes with array of paths [#203](https://github.com/alexmingoia/koa-router/issue/143).
    -* Improved router.url() [#143](https://github.com/alexmingoia/koa-router/pull/143)
    -* Adds support for named routes and regular expressions
    -  [#152](https://github.com/alexmingoia/koa-router/pull/152)
    -* Add support for custom throw functions for 405 and 501 responses [#206](https://github.com/alexmingoia/koa-router/pull/206)
    -
    -
    -## 5.2.3
    -
    -* Fix for middleware running twice when nesting routes [#184](https://github.com/alexmingoia/koa-router/issues/184)
    -
    -
    -## 5.2.2
    -
    -* Register routes without params before those with params [#183](https://github.com/alexmingoia/koa-router/pull/183)
    -* Fix for allowed methods [#182](https://github.com/alexmingoia/koa-router/issues/182)
    -
    -
    -## 5.2.0
    -
    -* Add support for async/await. Resolves [#130](https://github.com/alexmingoia/koa-router/issues/130).
    -* Add support for array of paths by Router#use(). Resolves [#175](https://github.com/alexmingoia/koa-router/issues/175).
    -* Inherit param middleware when nesting routers. Fixes [#170](https://github.com/alexmingoia/koa-router/issues/170).
    -* Default router middleware without path to root. Fixes [#161](https://github.com/alexmingoia/koa-router/issues/161), [#155](https://github.com/alexmingoia/koa-router/issues/155), [#156](https://github.com/alexmingoia/koa-router/issues/156).
    -* Run nested router middleware after parent's. Fixes [#156](https://github.com/alexmingoia/koa-router/issues/156).
    -* Remove dependency on koa-compose.
    -
    -
    -## 5.1.1
    -
    -* Match routes in order they were defined. Fixes #131.
    -
    -
    -## 5.1.0
    -
    -* Support mounting router middleware at a given path.
    -
    -
    -## 5.0.1
    -
    -* Fix bug with missing parameters when nesting routers.
    -
    -
    -## 5.0.0
    -
    -* Remove confusing API for extending koa app with router methods. Router#use()
    -  does not have the same behavior as app#use().
    -* Add support for nesting routes.
    -* Remove support for regular expression routes to achieve nestable routers and
    -  enable future trie-based routing optimizations.
    -
    -
    -## 4.3.2
    -
    -* Do not send 405 if route matched but status is 404. Fixes #112, closes #114.
    -
    -
    -## 4.3.1
    -
    -* Do not run middleware if not yielded to by previous middleware. Fixes #115.
    -
    -
    -## 4.3.0
    -
    -* Add support for router prefixes.
    -* Add MIT license.
    -
    -
    -## 4.2.0
    -
    -* Fixed issue with router middleware being applied even if no route was
    -  matched.
    -* Router.url - new static method to generate url from url pattern and data
    -
    -
    -## 4.1.0
    -
    -Private API changed to separate context parameter decoration from route
    -matching. `Router#match` and `Route#match` are now pure functions that return
    -an array of routes that match the URL path.
    -
    -For modules using this private API that need to determine if a method and path
    -match a route, `route.methods` must be checked against the routes returned from
    -`router.match()`:
    -
    -```javascript
    -var matchedRoute = router.match(path).filter(function (route) {
    -  return ~route.methods.indexOf(method);
    -}).shift();
    -```
    -
    -
    -## 4.0.0
    -
    -405, 501, and OPTIONS response handling was moved into separate middleware
    -`router.allowedMethods()`. This resolves incorrect 501 or 405 responses when
    -using multiple routers.
    -
    -### Breaking changes
    -
    -4.x is mostly backwards compatible with 3.x, except for the following:
    -
    -* Instantiating a router with `new` and `app` returns the router instance,
    -  whereas 3.x returns the router middleware. When creating a router in 4.x, the
    -  only time router middleware is returned is when creating using the
    -  `Router(app)` signature (with `app` and without `new`).
    
  • .husky/pre-commit+1 1 modified
    @@ -1,4 +1,4 @@
     #!/bin/sh
     . "$(dirname "$0")/_/husky.sh"
     
    -npx --no-install lint-staged && npm test
    +npx --no-install lint-staged && npm run test:all
    
  • lib/API_tpl.hbs+0 7 removed
    @@ -1,7 +0,0 @@
    -
    -## API Reference
    -{{#module name="koa-router"~}}
    -  {{>body~}}
    -  {{>member-index~}}
    -  {{>members~}}
    -{{/module~}}
    
  • lib/layer.js+0 262 removed
    @@ -1,262 +0,0 @@
    -const { parse: parseUrl, format: formatUrl } = require('node:url');
    -
    -const { pathToRegexp, compile, parse } = require('path-to-regexp');
    -
    -module.exports = class Layer {
    -  /**
    -   * Initialize a new routing Layer with given `method`, `path`, and `middleware`.
    -   *
    -   * @param {String|RegExp} path Path string or regular expression.
    -   * @param {Array} methods Array of HTTP verbs.
    -   * @param {Array} middleware Layer callback/middleware or series of.
    -   * @param {Object=} opts
    -   * @param {String=} opts.name route name
    -   * @param {String=} opts.sensitive case sensitive (default: false)
    -   * @param {String=} opts.strict require the trailing slash (default: false)
    -   * @param {Boolean=} opts.pathAsRegExp if true, treat `path` as a regular expression
    -   * @returns {Layer}
    -   * @private
    -   */
    -  constructor(path, methods, middleware, opts = {}) {
    -    this.opts = opts;
    -    this.name = this.opts.name || null;
    -    this.methods = [];
    -    for (const method of methods) {
    -      const l = this.methods.push(method.toUpperCase());
    -      if (this.methods[l - 1] === 'GET') this.methods.unshift('HEAD');
    -    }
    -
    -    this.stack = Array.isArray(middleware) ? middleware : [middleware];
    -    // ensure middleware is a function
    -    for (let i = 0; i < this.stack.length; i++) {
    -      const fn = this.stack[i];
    -      const type = typeof fn;
    -      if (type !== 'function')
    -        throw new Error(
    -          `${methods.toString()} \`${
    -            this.opts.name || path
    -          }\`: \`middleware\` must be a function, not \`${type}\``
    -        );
    -    }
    -
    -    this.path = path;
    -    this.paramNames = [];
    -
    -    if (this.opts.pathAsRegExp === true) {
    -      this.regexp = new RegExp(path);
    -    } else if (this.path) {
    -      if ('strict' in this.opts) {
    -        // path-to-regexp renamed strict to trailing in v8.1.0
    -        this.opts.trailing = this.opts.strict !== true;
    -      }
    -
    -      const { regexp, keys } = pathToRegexp(this.path, this.opts);
    -      this.regexp = regexp;
    -      this.paramNames = keys;
    -    }
    -  }
    -
    -  /**
    -   * Returns whether request `path` matches route.
    -   *
    -   * @param {String} path
    -   * @returns {Boolean}
    -   * @private
    -   */
    -  match(path) {
    -    return this.regexp.test(path);
    -  }
    -
    -  /**
    -   * Returns map of URL parameters for given `path` and `paramNames`.
    -   *
    -   * @param {String} path
    -   * @param {Array.<String>} captures
    -   * @param {Object=} params
    -   * @returns {Object}
    -   * @private
    -   */
    -  params(path, captures, params = {}) {
    -    for (let len = captures.length, i = 0; i < len; i++) {
    -      if (this.paramNames[i]) {
    -        const c = captures[i];
    -        if (c && c.length > 0)
    -          params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c;
    -      }
    -    }
    -
    -    return params;
    -  }
    -
    -  /**
    -   * Returns array of regexp url path captures.
    -   *
    -   * @param {String} path
    -   * @returns {Array.<String>}
    -   * @private
    -   */
    -  captures(path) {
    -    return this.opts.ignoreCaptures ? [] : path.match(this.regexp).slice(1);
    -  }
    -
    -  /**
    -   * Generate URL for route using given `params`.
    -   *
    -   * @example
    -   *
    -   * ```javascript
    -   * const route = new Layer('/users/:id', ['GET'], fn);
    -   *
    -   * route.url({ id: 123 }); // => "/users/123"
    -   * ```
    -   *
    -   * @param {Object} params url parameters
    -   * @returns {String}
    -   * @private
    -   */
    -  url(params, options) {
    -    let args = params;
    -    const url = this.path.replace(/\(\.\*\)/g, '');
    -
    -    if (typeof params !== 'object') {
    -      args = Array.prototype.slice.call(arguments);
    -      if (typeof args[args.length - 1] === 'object') {
    -        options = args[args.length - 1];
    -        args = args.slice(0, -1);
    -      }
    -    }
    -
    -    const toPath = compile(url, { encode: encodeURIComponent, ...options });
    -    let replaced;
    -    const { tokens } = parse(url);
    -    let replace = {};
    -
    -    if (Array.isArray(args)) {
    -      for (let len = tokens.length, i = 0, j = 0; i < len; i++) {
    -        if (tokens[i].name) {
    -          replace[tokens[i].name] = args[j++];
    -        }
    -      }
    -    } else if (tokens.some((token) => token.name)) {
    -      replace = params;
    -    } else if (!options) {
    -      options = params;
    -    }
    -
    -    for (const [key, value] of Object.entries(replace)) {
    -      replace[key] = String(value);
    -    }
    -
    -    replaced = toPath(replace);
    -
    -    if (options && options.query) {
    -      replaced = parseUrl(replaced);
    -      if (typeof options.query === 'string') {
    -        replaced.search = options.query;
    -      } else {
    -        replaced.search = undefined;
    -        replaced.query = options.query;
    -      }
    -
    -      return formatUrl(replaced);
    -    }
    -
    -    return replaced;
    -  }
    -
    -  /**
    -   * Run validations on route named parameters.
    -   *
    -   * @example
    -   *
    -   * ```javascript
    -   * router
    -   *   .param('user', function (id, ctx, next) {
    -   *     ctx.user = users[id];
    -   *     if (!ctx.user) return ctx.status = 404;
    -   *     next();
    -   *   })
    -   *   .get('/users/:user', function (ctx, next) {
    -   *     ctx.body = ctx.user;
    -   *   });
    -   * ```
    -   *
    -   * @param {String} param
    -   * @param {Function} middleware
    -   * @returns {Layer}
    -   * @private
    -   */
    -  param(param, fn) {
    -    const { stack } = this;
    -    const params = this.paramNames;
    -    const middleware = function (ctx, next) {
    -      return fn.call(this, ctx.params[param], ctx, next);
    -    };
    -
    -    middleware.param = param;
    -
    -    const names = params.map(function (p) {
    -      return p.name;
    -    });
    -
    -    const x = names.indexOf(param);
    -    if (x > -1) {
    -      // iterate through the stack, to figure out where to place the handler fn
    -      stack.some((fn, i) => {
    -        // param handlers are always first, so when we find an fn w/o a param property, stop here
    -        // if the param handler at this part of the stack comes after the one we are adding, stop here
    -        if (!fn.param || names.indexOf(fn.param) > x) {
    -          // inject this param handler right before the current item
    -          stack.splice(i, 0, middleware);
    -          return true; // then break the loop
    -        }
    -      });
    -    }
    -
    -    return this;
    -  }
    -
    -  /**
    -   * Prefix route path.
    -   *
    -   * @param {String} prefix
    -   * @returns {Layer}
    -   * @private
    -   */
    -  setPrefix(prefix) {
    -    if (this.path) {
    -      this.path =
    -        this.path !== '/' || this.opts.strict === true
    -          ? `${prefix}${this.path}`
    -          : prefix;
    -      if (this.opts.pathAsRegExp === true || prefix instanceof RegExp) {
    -        this.regexp = new RegExp(this.path);
    -      } else if (this.path) {
    -        const { regexp, keys } = pathToRegexp(this.path, this.opts);
    -        this.regexp = regexp;
    -        this.paramNames = keys;
    -      }
    -    }
    -
    -    return this;
    -  }
    -};
    -
    -/**
    - * Safe decodeURIComponent, won't throw any error.
    - * If `decodeURIComponent` error happen, just return the original value.
    - *
    - * @param {String} text
    - * @returns {String} URL decode original string.
    - * @private
    - */
    -
    -function safeDecodeURIComponent(text) {
    -  try {
    -    // TODO: take a look on `safeDecodeURIComponent` if we use it only with route params let's remove the `replace` method otherwise make it flexible.
    -    // @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url
    -    return decodeURIComponent(text.replace(/\+/g, ' '));
    -  } catch {
    -    return text;
    -  }
    -}
    
  • lib/router.js+0 835 removed
    @@ -1,835 +0,0 @@
    -/**
    - * RESTful resource routing middleware for koa.
    - *
    - * @author Alex Mingoia <talk@alexmingoia.com>
    - * @link https://github.com/alexmingoia/koa-router
    - */
    -const http = require('node:http');
    -
    -const debug = require('debug')('koa-router');
    -
    -const compose = require('koa-compose');
    -const HttpError = require('http-errors');
    -const { pathToRegexp } = require('path-to-regexp');
    -
    -const Layer = require('./layer');
    -
    -const methods = http.METHODS.map((method) => method.toLowerCase());
    -
    -/**
    - * @module koa-router
    - */
    -class Router {
    -  /**
    -   * Create a new router.
    -   *
    -   * @example
    -   *
    -   * Basic usage:
    -   *
    -   * ```javascript
    -   * const Koa = require('koa');
    -   * const Router = require('@koa/router');
    -   *
    -   * const app = new Koa();
    -   * const router = new Router();
    -   *
    -   * router.get('/', (ctx, next) => {
    -   *   // ctx.router available
    -   * });
    -   *
    -   * app
    -   *   .use(router.routes())
    -   *   .use(router.allowedMethods());
    -   * ```
    -   *
    -   * @alias module:koa-router
    -   * @param {Object=} opts
    -   * @param {Boolean=false} opts.exclusive only run last matched route's controller when there are multiple matches
    -   * @param {String=} opts.prefix prefix router paths
    -   * @param {String|RegExp=} opts.host host for router match
    -   * @constructor
    -   */
    -  constructor(opts = {}) {
    -    if (!(this instanceof Router)) return new Router(opts);  
    -
    -    this.opts = opts;
    -    this.methods = this.opts.methods || [
    -      'HEAD',
    -      'OPTIONS',
    -      'GET',
    -      'PUT',
    -      'PATCH',
    -      'POST',
    -      'DELETE'
    -    ];
    -    this.exclusive = Boolean(this.opts.exclusive);
    -
    -    this.params = {};
    -    this.stack = [];
    -    this.host = this.opts.host;
    -  }
    -
    -  /**
    -   * Generate URL from url pattern and given `params`.
    -   *
    -   * @example
    -   *
    -   * ```javascript
    -   * const url = Router.url('/users/:id', {id: 1});
    -   * // => "/users/1"
    -   * ```
    -   *
    -   * @param {String} path url pattern
    -   * @param {Object} params url parameters
    -   * @returns {String}
    -   */
    -  static url(path, ...args) {
    -    return Layer.prototype.url.apply({ path }, args);
    -  }
    -
    -  /**
    -   * Use given middleware.
    -   *
    -   * Middleware run in the order they are defined by `.use()`. They are invoked
    -   * sequentially, requests start at the first middleware and work their way
    -   * "down" the middleware stack.
    -   *
    -   * @example
    -   *
    -   * ```javascript
    -   * // session middleware will run before authorize
    -   * router
    -   *   .use(session())
    -   *   .use(authorize());
    -   *
    -   * // use middleware only with given path
    -   * router.use('/users', userAuth());
    -   *
    -   * // or with an array of paths
    -   * router.use(['/users', '/admin'], userAuth());
    -   *
    -   * app.use(router.routes());
    -   * ```
    -   *
    -   * @param {String=} path
    -   * @param {Function} middleware
    -   * @param {Function=} ...
    -   * @returns {Router}
    -   */
    -  use(...middleware) {
    -    const router = this;
    -    let path;
    -
    -    // support array of paths
    -    if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
    -      const arrPaths = middleware[0];
    -      for (const p of arrPaths) {
    -        router.use.apply(router, [p, ...middleware.slice(1)]);
    -      }
    -
    -      return this;
    -    }
    -
    -    const hasPath = typeof middleware[0] === 'string';
    -    if (hasPath) path = middleware.shift();
    -
    -    for (const m of middleware) {
    -      if (m.router) {
    -        const cloneRouter = Object.assign(
    -          Object.create(Router.prototype),
    -          m.router,
    -          {
    -            stack: [...m.router.stack]
    -          }
    -        );
    -
    -        for (let j = 0; j < cloneRouter.stack.length; j++) {
    -          const nestedLayer = cloneRouter.stack[j];
    -          const cloneLayer = Object.assign(
    -            Object.create(Layer.prototype),
    -            nestedLayer
    -          );
    -
    -          if (path) cloneLayer.setPrefix(path);
    -          if (router.opts.prefix) cloneLayer.setPrefix(router.opts.prefix);
    -          router.stack.push(cloneLayer);
    -          cloneRouter.stack[j] = cloneLayer;
    -        }
    -
    -        if (router.params) {
    -          const routerParams = Object.keys(router.params);
    -          for (const key of routerParams) {
    -            cloneRouter.param(key, router.params[key]);
    -          }
    -        }
    -      } else {
    -        const { keys } = pathToRegexp(router.opts.prefix || '', router.opts);
    -        const routerPrefixHasParam = Boolean(
    -          router.opts.prefix && keys.length > 0
    -        );
    -        router.register(path || '([^/]*)', [], m, {
    -          end: false,
    -          ignoreCaptures: !hasPath && !routerPrefixHasParam,
    -          pathAsRegExp: true
    -        });
    -      }
    -    }
    -
    -    return this;
    -  }
    -
    -  /**
    -   * Set the path prefix for a Router instance that was already initialized.
    -   *
    -   * @example
    -   *
    -   * ```javascript
    -   * router.prefix('/things/:thing_id')
    -   * ```
    -   *
    -   * @param {String} prefix
    -   * @returns {Router}
    -   */
    -  prefix(prefix) {
    -    prefix = prefix.replace(/\/$/, '');
    -
    -    this.opts.prefix = prefix;
    -
    -    for (let i = 0; i < this.stack.length; i++) {
    -      const route = this.stack[i];
    -      route.setPrefix(prefix);
    -    }
    -
    -    return this;
    -  }
    -
    -  /**
    -   * Returns router middleware which dispatches a route matching the request.
    -   *
    -   * @returns {Function}
    -   */
    -  middleware() {
    -    const router = this;
    -    const dispatch = (ctx, next) => {
    -      debug('%s %s', ctx.method, ctx.path);
    -
    -      const hostMatched = router.matchHost(ctx.host);
    -
    -      if (!hostMatched) {
    -        return next();
    -      }
    -
    -      const path =
    -        router.opts.routerPath ||
    -        ctx.newRouterPath ||
    -        ctx.path ||
    -        ctx.routerPath;
    -      const matched = router.match(path, ctx.method);
    -      if (ctx.matched) {
    -        ctx.matched.push.apply(ctx.matched, matched.path);
    -      } else {
    -        ctx.matched = matched.path;
    -      }
    -
    -      ctx.router = router;
    -
    -      if (!matched.route) return next();
    -
    -      const matchedLayers = matched.pathAndMethod;
    -      const mostSpecificLayer = matchedLayers[matchedLayers.length - 1];
    -      ctx._matchedRoute = mostSpecificLayer.path;
    -      if (mostSpecificLayer.name) {
    -        ctx._matchedRouteName = mostSpecificLayer.name;
    -      }
    -
    -      const layerChain = (
    -        router.exclusive ? [mostSpecificLayer] : matchedLayers
    -      ).reduce((memo, layer) => {
    -        memo.push((ctx, next) => {
    -          ctx.captures = layer.captures(path, ctx.captures);
    -          ctx.request.params = layer.params(path, ctx.captures, ctx.params);
    -          ctx.params = ctx.request.params;
    -          ctx.routerPath = layer.path;
    -          ctx.routerName = layer.name;
    -          ctx._matchedRoute = layer.path;
    -          if (layer.name) {
    -            ctx._matchedRouteName = layer.name;
    -          }
    -
    -          return next();
    -        });
    -        return [...memo, ...layer.stack];
    -      }, []);
    -
    -      return compose(layerChain)(ctx, next);
    -    };
    -
    -    dispatch.router = this;
    -
    -    return dispatch;
    -  }
    -
    -  routes() {
    -    return this.middleware();
    -  }
    -
    -  /**
    -   * Returns separate middleware for responding to `OPTIONS` requests with
    -   * an `Allow` header containing the allowed methods, as well as responding
    -   * with `405 Method Not Allowed` and `501 Not Implemented` as appropriate.
    -   *
    -   * @example
    -   *
    -   * ```javascript
    -   * const Koa = require('koa');
    -   * const Router = require('@koa/router');
    -   *
    -   * const app = new Koa();
    -   * const router = new Router();
    -   *
    -   * app.use(router.routes());
    -   * app.use(router.allowedMethods());
    -   * ```
    -   *
    -   * **Example with [Boom](https://github.com/hapijs/boom)**
    -   *
    -   * ```javascript
    -   * const Koa = require('koa');
    -   * const Router = require('@koa/router');
    -   * const Boom = require('boom');
    -   *
    -   * const app = new Koa();
    -   * const router = new Router();
    -   *
    -   * app.use(router.routes());
    -   * app.use(router.allowedMethods({
    -   *   throw: true,
    -   *   notImplemented: () => new Boom.notImplemented(),
    -   *   methodNotAllowed: () => new Boom.methodNotAllowed()
    -   * }));
    -   * ```
    -   *
    -   * @param {Object=} options
    -   * @param {Boolean=} options.throw throw error instead of setting status and header
    -   * @param {Function=} options.notImplemented throw the returned value in place of the default NotImplemented error
    -   * @param {Function=} options.methodNotAllowed throw the returned value in place of the default MethodNotAllowed error
    -   * @returns {Function}
    -   */
    -  allowedMethods(options = {}) {
    -    const implemented = this.methods;
    -
    -    return (ctx, next) => {
    -      return next().then(() => {
    -        const allowed = {};
    -
    -        if (ctx.matched && (!ctx.status || ctx.status === 404)) {
    -          for (let i = 0; i < ctx.matched.length; i++) {
    -            const route = ctx.matched[i];
    -            for (let j = 0; j < route.methods.length; j++) {
    -              const method = route.methods[j];
    -              allowed[method] = method;
    -            }
    -          }
    -
    -          const allowedArr = Object.keys(allowed);
    -          if (!implemented.includes(ctx.method)) {
    -            if (options.throw) {
    -              const notImplementedThrowable =
    -                typeof options.notImplemented === 'function'
    -                  ? options.notImplemented() // set whatever the user returns from their function
    -                  : new HttpError.NotImplemented();
    -
    -              throw notImplementedThrowable;
    -            } else {
    -              ctx.status = 501;
    -              ctx.set('Allow', allowedArr.join(', '));
    -            }
    -          } else if (allowedArr.length > 0) {
    -            if (ctx.method === 'OPTIONS') {
    -              ctx.status = 200;
    -              ctx.body = '';
    -              ctx.set('Allow', allowedArr.join(', '));
    -            } else if (!allowed[ctx.method]) {
    -              if (options.throw) {
    -                const notAllowedThrowable =
    -                  typeof options.methodNotAllowed === 'function'
    -                    ? options.methodNotAllowed() // set whatever the user returns from their function
    -                    : new HttpError.MethodNotAllowed();
    -
    -                throw notAllowedThrowable;
    -              } else {
    -                ctx.status = 405;
    -                ctx.set('Allow', allowedArr.join(', '));
    -              }
    -            }
    -          }
    -        }
    -      });
    -    };
    -  }
    -
    -  /**
    -   * Register route with all methods.
    -   *
    -   * @param {String} name Optional.
    -   * @param {String} path
    -   * @param {Function=} middleware You may also pass multiple middleware.
    -   * @param {Function} callback
    -   * @returns {Router}
    -   */
    -  all(name, path, middleware) {
    -    if (typeof path === 'string' || path instanceof RegExp) {
    -      middleware = Array.prototype.slice.call(arguments, 2);
    -    } else {
    -      middleware = Array.prototype.slice.call(arguments, 1);
    -      path = name;
    -      name = null;
    -    }
    -
    -    // Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array)
    -    if (
    -      typeof path !== 'string' &&
    -      !(path instanceof RegExp) &&
    -      (!Array.isArray(path) || path.length === 0)
    -    )
    -      throw new Error('You have to provide a path when adding an all handler');
    -
    -    const opts = {
    -      name,
    -      pathAsRegExp: path instanceof RegExp
    -    };
    -
    -    this.register(path, methods, middleware, { ...this.opts, ...opts });
    -
    -    return this;
    -  }
    -
    -  /**
    -   * Redirect `source` to `destination` URL with optional 30x status `code`.
    -   *
    -   * Both `source` and `destination` can be route names.
    -   *
    -   * ```javascript
    -   * router.redirect('/login', 'sign-in');
    -   * ```
    -   *
    -   * This is equivalent to:
    -   *
    -   * ```javascript
    -   * router.all('/login', ctx => {
    -   *   ctx.redirect('/sign-in');
    -   *   ctx.status = 301;
    -   * });
    -   * ```
    -   *
    -   * @param {String} source URL or route name.
    -   * @param {String} destination URL or route name.
    -   * @param {Number=} code HTTP status code (default: 301).
    -   * @returns {Router}
    -   */
    -  redirect(source, destination, code) {
    -    // lookup source route by name
    -    if (typeof source === 'symbol' || source[0] !== '/') {
    -      source = this.url(source);
    -      if (source instanceof Error) throw source;
    -    }
    -
    -    // lookup destination route by name
    -    if (
    -      typeof destination === 'symbol' ||
    -      (destination[0] !== '/' && !destination.includes('://'))
    -    ) {
    -      destination = this.url(destination);
    -      if (destination instanceof Error) throw destination;
    -    }
    -
    -    return this.all(source, (ctx) => {
    -      ctx.redirect(destination);
    -      ctx.status = code || 301;
    -    });
    -  }
    -
    -  /**
    -   * Create and register a route.
    -   *
    -   * @param {String} path Path string.
    -   * @param {Array.<String>} methods Array of HTTP verbs.
    -   * @param {Function} middleware Multiple middleware also accepted.
    -   * @returns {Layer}
    -   * @private
    -   */
    -  register(path, methods, middleware, newOpts = {}) {
    -    const router = this;
    -    const { stack } = this;
    -    const opts = { ...this.opts, ...newOpts };
    -    // support array of paths
    -    if (Array.isArray(path)) {
    -      for (const curPath of path) {
    -        router.register.call(router, curPath, methods, middleware, opts);
    -      }
    -
    -      return this;
    -    }
    -
    -    // create route
    -    const route = new Layer(path, methods, middleware, {
    -      end: opts.end === false ? opts.end : true,
    -      name: opts.name,
    -      sensitive: opts.sensitive || false,
    -      strict: opts.strict || false,
    -      prefix: opts.prefix || '',
    -      ignoreCaptures: opts.ignoreCaptures,
    -      pathAsRegExp: opts.pathAsRegExp
    -    });
    -
    -    // if parent prefix exists, add prefix to new route
    -    if (this.opts.prefix) {
    -      route.setPrefix(this.opts.prefix);
    -    }
    -
    -    // add parameter middleware
    -    for (let i = 0; i < Object.keys(this.params).length; i++) {
    -      const param = Object.keys(this.params)[i];
    -      route.param(param, this.params[param]);
    -    }
    -
    -    stack.push(route);
    -
    -    debug('defined route %s %s', route.methods, route.path);
    -
    -    return route;
    -  }
    -
    -  /**
    -   * Lookup route with given `name`.
    -   *
    -   * @param {String} name
    -   * @returns {Layer|false}
    -   */
    -  route(name) {
    -    const routes = this.stack;
    -
    -    for (let len = routes.length, i = 0; i < len; i++) {
    -      if (routes[i].name && routes[i].name === name) return routes[i];
    -    }
    -
    -    return false;
    -  }
    -
    -  /**
    -   * Generate URL for route. Takes a route name and map of named `params`.
    -   *
    -   * @example
    -   *
    -   * ```javascript
    -   * router.get('user', '/users/:id', (ctx, next) => {
    -   *   // ...
    -   * });
    -   *
    -   * router.url('user', 3);
    -   * // => "/users/3"
    -   *
    -   * router.url('user', { id: 3 });
    -   * // => "/users/3"
    -   *
    -   * router.use((ctx, next) => {
    -   *   // redirect to named route
    -   *   ctx.redirect(ctx.router.url('sign-in'));
    -   * })
    -   *
    -   * router.url('user', { id: 3 }, { query: { limit: 1 } });
    -   * // => "/users/3?limit=1"
    -   *
    -   * router.url('user', { id: 3 }, { query: "limit=1" });
    -   * // => "/users/3?limit=1"
    -   * ```
    -   *
    -   * @param {String} name route name
    -   * @param {Object} params url parameters
    -   * @param {Object} [options] options parameter
    -   * @param {Object|String} [options.query] query options
    -   * @returns {String|Error}
    -   */
    -  url(name, ...args) {
    -    const route = this.route(name);
    -    if (route) return route.url.apply(route, args);
    -
    -    return new Error(`No route found for name: ${String(name)}`);
    -  }
    -
    -  /**
    -   * Match given `path` and return corresponding routes.
    -   *
    -   * @param {String} path
    -   * @param {String} method
    -   * @returns {Object.<path, pathAndMethod>} returns layers that matched path and
    -   * path and method.
    -   * @private
    -   */
    -  match(path, method) {
    -    const layers = this.stack;
    -    let layer;
    -    const matched = {
    -      path: [],
    -      pathAndMethod: [],
    -      route: false
    -    };
    -
    -    for (let len = layers.length, i = 0; i < len; i++) {
    -      layer = layers[i];
    -
    -      debug('test %s %s', layer.path, layer.regexp);
    -
    -      if (layer.match(path)) {
    -        matched.path.push(layer);
    -
    -        if (layer.methods.length === 0 || layer.methods.includes(method)) {
    -          matched.pathAndMethod.push(layer);
    -          if (layer.methods.length > 0) matched.route = true;
    -        }
    -      }
    -    }
    -
    -    return matched;
    -  }
    -
    -  /**
    -   * Match given `input` to allowed host
    -   * @param {String} input
    -   * @returns {boolean}
    -   */
    -  matchHost(input) {
    -    const { host } = this;
    -
    -    if (!host) {
    -      return true;
    -    }
    -
    -    if (!input) {
    -      return false;
    -    }
    -
    -    if (typeof host === 'string') {
    -      return input === host;
    -    }
    -
    -    if (typeof host === 'object' && host instanceof RegExp) {
    -      return host.test(input);
    -    }
    -  }
    -
    -  /**
    -   * Run middleware for named route parameters. Useful for auto-loading or
    -   * validation.
    -   *
    -   * @example
    -   *
    -   * ```javascript
    -   * router
    -   *   .param('user', (id, ctx, next) => {
    -   *     ctx.user = users[id];
    -   *     if (!ctx.user) return ctx.status = 404;
    -   *     return next();
    -   *   })
    -   *   .get('/users/:user', ctx => {
    -   *     ctx.body = ctx.user;
    -   *   })
    -   *   .get('/users/:user/friends', ctx => {
    -   *     return ctx.user.getFriends().then(function(friends) {
    -   *       ctx.body = friends;
    -   *     });
    -   *   })
    -   *   // /users/3 => {"id": 3, "name": "Alex"}
    -   *   // /users/3/friends => [{"id": 4, "name": "TJ"}]
    -   * ```
    -   *
    -   * @param {String} param
    -   * @param {Function} middleware
    -   * @returns {Router}
    -   */
    -  param(param, middleware) {
    -    this.params[param] = middleware;
    -    for (let i = 0; i < this.stack.length; i++) {
    -      const route = this.stack[i];
    -      route.param(param, middleware);
    -    }
    -
    -    return this;
    -  }
    -}
    -
    -/**
    - * Create `router.verb()` methods, where *verb* is one of the HTTP verbs such
    - * as `router.get()` or `router.post()`.
    - *
    - * Match URL patterns to callback functions or controller actions using `router.verb()`,
    - * where **verb** is one of the HTTP verbs such as `router.get()` or `router.post()`.
    - *
    - * Additionally, `router.all()` can be used to match against all methods.
    - *
    - * ```javascript
    - * router
    - *   .get('/', (ctx, next) => {
    - *     ctx.body = 'Hello World!';
    - *   })
    - *   .post('/users', (ctx, next) => {
    - *     // ...
    - *   })
    - *   .put('/users/:id', (ctx, next) => {
    - *     // ...
    - *   })
    - *   .del('/users/:id', (ctx, next) => {
    - *     // ...
    - *   })
    - *   .all('/users/:id', (ctx, next) => {
    - *     // ...
    - *   });
    - * ```
    - *
    - * When a route is matched, its path is available at `ctx._matchedRoute` and if named,
    - * the name is available at `ctx._matchedRouteName`
    - *
    - * Route paths will be translated to regular expressions using
    - * [path-to-regexp](https://github.com/pillarjs/path-to-regexp).
    - *
    - * Query strings will not be considered when matching requests.
    - *
    - * #### Named routes
    - *
    - * Routes can optionally have names. This allows generation of URLs and easy
    - * renaming of URLs during development.
    - *
    - * ```javascript
    - * router.get('user', '/users/:id', (ctx, next) => {
    - *  // ...
    - * });
    - *
    - * router.url('user', 3);
    - * // => "/users/3"
    - * ```
    - *
    - * #### Multiple middleware
    - *
    - * Multiple middleware may be given:
    - *
    - * ```javascript
    - * router.get(
    - *   '/users/:id',
    - *   (ctx, next) => {
    - *     return User.findOne(ctx.params.id).then(function(user) {
    - *       ctx.user = user;
    - *       next();
    - *     });
    - *   },
    - *   ctx => {
    - *     console.log(ctx.user);
    - *     // => { id: 17, name: "Alex" }
    - *   }
    - * );
    - * ```
    - *
    - * ### Nested routers
    - *
    - * Nesting routers is supported:
    - *
    - * ```javascript
    - * const forums = new Router();
    - * const posts = new Router();
    - *
    - * posts.get('/', (ctx, next) => {...});
    - * posts.get('/:pid', (ctx, next) => {...});
    - * forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());
    - *
    - * // responds to "/forums/123/posts" and "/forums/123/posts/123"
    - * app.use(forums.routes());
    - * ```
    - *
    - * #### Router prefixes
    - *
    - * Route paths can be prefixed at the router level:
    - *
    - * ```javascript
    - * const router = new Router({
    - *   prefix: '/users'
    - * });
    - *
    - * router.get('/', ...); // responds to "/users"
    - * router.get('/:id', ...); // responds to "/users/:id"
    - * ```
    - *
    - * #### URL parameters
    - *
    - * Named route parameters are captured and added to `ctx.params`.
    - *
    - * ```javascript
    - * router.get('/:category/:title', (ctx, next) => {
    - *   console.log(ctx.params);
    - *   // => { category: 'programming', title: 'how-to-node' }
    - * });
    - * ```
    - *
    - * The [path-to-regexp](https://github.com/pillarjs/path-to-regexp) module is
    - * used to convert paths to regular expressions.
    - *
    - *
    - * ### Match host for each router instance
    - *
    - * ```javascript
    - * const router = new Router({
    - *    host: 'example.domain' // only match if request host exactly equal `example.domain`
    - * });
    - *
    - * ```
    - *
    - * OR host cloud be a regexp
    - *
    - * ```javascript
    - * const router = new Router({
    - *     host: /.*\.?example\.domain$/ // all host end with .example.domain would be matched
    - * });
    - * ```
    - *
    - * @name get|put|post|patch|delete|del
    - * @memberof module:koa-router.prototype
    - * @param {String} path
    - * @param {Function=} middleware route middleware(s)
    - * @param {Function} callback route callback
    - * @returns {Router}
    - */
    -for (const method of methods) {
    -  Router.prototype[method] = function (name, path, middleware) {
    -    if (typeof path === 'string' || path instanceof RegExp) {
    -      middleware = Array.prototype.slice.call(arguments, 2);
    -    } else {
    -      middleware = Array.prototype.slice.call(arguments, 1);
    -      path = name;
    -      name = null;
    -    }
    -
    -    // Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array)
    -    if (
    -      typeof path !== 'string' &&
    -      !(path instanceof RegExp) &&
    -      (!Array.isArray(path) || path.length === 0)
    -    )
    -      throw new Error(
    -        `You have to provide a path when adding a ${method} handler`
    -      );
    -
    -    const opts = {
    -      name,
    -      pathAsRegExp: path instanceof RegExp
    -    };
    -
    -    // pass opts to register call on verb methods
    -    this.register(path, [method], middleware, { ...this.opts, ...opts });
    -    return this;
    -  };
    -}
    -
    -// Alias for `router.delete()` because delete is a reserved word
    - 
    -Router.prototype.del = Router.prototype['delete'];
    -
    -module.exports = Router;
    
  • LICENSE+1 1 modified
    @@ -1,6 +1,6 @@
     The MIT License (MIT)
     
    -Copyright (c) 2015 Alexander C. Mingoia and @koajs contributors
    +Copyright (c) 2015 @koajs maintainers and contributors
     
     Permission is hereby granted, free of charge, to any person obtaining a copy
     of this software and associated documentation files (the "Software"), to deal
    
  • .lintstagedrc.js+0 5 removed
    @@ -1,5 +0,0 @@
    -module.exports = {
    -  '*.md': (filenames) => filenames.map((filename) => `remark ${filename} -qfo`),
    -  'package.json': 'fixpack',
    -  '*.js': 'eslint . --fix'
    -};
    
  • .lintstagedrc.json+5 0 added
    @@ -0,0 +1,5 @@
    +{
    +  "*.{ts}": ["prettier --write", "eslint --fix"],
    +  "*.{js}": ["prettier --write", "eslint --fix"],
    +  "*.{json,md,yml,yaml}": ["prettier --write"]
    +}
    
  • .nvmrc+1 0 added
    @@ -0,0 +1 @@
    +v24.6.0
    
  • package.json+54 27 modified
    @@ -21,34 +21,53 @@
         }
       ],
       "dependencies": {
    -    "debug": "^4.4.1",
    -    "http-errors": "^2.0.0",
    +    "debug": "^4.4.3",
    +    "http-errors": "^2.0.1",
         "koa-compose": "^4.1.0",
    -    "path-to-regexp": "^8.2.0"
    +    "path-to-regexp": "^8.3.0"
       },
       "devDependencies": {
    -    "@commitlint/cli": "^17.7.2",
    -    "@commitlint/config-conventional": "^17.7.0",
    -    "@ladjs/env": "^4.0.0",
    +    "@commitlint/cli": "^20.1.0",
    +    "@commitlint/config-conventional": "^20.0.0",
    +    "@types/debug": "^4.1.12",
    +    "@types/jsonwebtoken": "^9.0.7",
    +    "@types/koa": "^3.0.1",
    +    "@types/node": "^24.10.1",
    +    "@types/supertest": "^6.0.3",
    +    "@typescript-eslint/eslint-plugin": "^8.48.0",
    +    "@typescript-eslint/parser": "^8.48.0",
    +    "c8": "^10.1.3",
         "chalk": "^5.4.1",
    -    "eslint": "^9.32.0",
    -    "eslint-plugin-unicorn": "^60.0.0",
    -    "fixpack": "^4.0.0",
    +    "eslint": "^9.39.1",
    +    "eslint-plugin-unicorn": "^62.0.0",
         "husky": "^9.1.7",
    -    "jsdoc-to-markdown": "^8.0.0",
    -    "koa": "^3.0.1",
    -    "lint-staged": "^14.0.1",
    -    "mocha": "^11.7.1",
    -    "nyc": "^17.0.0",
    -    "remark-cli": "11",
    -    "remark-preset-github": "^4.0.4",
    -    "supertest": "^7.0.0"
    +    "joi": "^18.0.2",
    +    "jsonwebtoken": "^9.0.2",
    +    "koa": "^3.1.1",
    +    "lint-staged": "^16.2.7",
    +    "prettier": "^3.7.1",
    +    "rimraf": "^6.1.2",
    +    "supertest": "^7.1.4",
    +    "ts-node": "^10.9.2",
    +    "tsup": "^8.5.1",
    +    "typescript": "^5.9.3"
       },
       "engines": {
         "node": ">= 20"
       },
    +  "main": "./dist/index.js",
    +  "module": "./dist/index.mjs",
    +  "types": "./dist/index.d.ts",
    +  "exports": {
    +    ".": {
    +      "require": "./dist/index.js",
    +      "import": "./dist/index.mjs"
    +    }
    +  },
       "files": [
    -    "lib"
    +    "dist",
    +    "LICENSE",
    +    "README.md"
       ],
       "homepage": "https://github.com/koajs/router",
       "keywords": [
    @@ -58,20 +77,28 @@
         "router"
       ],
       "license": "MIT",
    -  "main": "lib/router.js",
       "repository": {
         "type": "git",
         "url": "git+https://github.com/koajs/router.git"
       },
       "scripts": {
    -    "bench": "make -C bench",
    -    "benchmark": "node bench/run.js",
    -    "coverage": "nyc npm run test",
    -    "docs": "NODE_ENV=test jsdoc2md -t ./lib/API_tpl.hbs --src ./lib/*.js  >| API.md",
    -    "lint": "eslint . --fix && remark . -qfo && fixpack",
    +    "bench": "TS_NODE_PROJECT=tsconfig.bench.json node --require ts-node/register bench/run.ts",
    +    "benchmark": "npm run bench",
    +    "bench:all": "TS_NODE_PROJECT=tsconfig.bench.json node --require ts-node/register bench/make.ts",
    +    "benchmark:all": "npm run bench:all",
         "prepare": "husky install",
    -    "pretest": "npm run lint",
    -    "test": "mocha test/**/*.js",
    -    "test:watch": "mocha test/**/*.js --watch"
    +    "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"",
    +    "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"",
    +    "lint:ts": "eslint src test --ext .ts,.tsx --fix",
    +    "lint": "npm run lint:ts",
    +    "test:core": "TS_NODE_PROJECT=tsconfig.ts-node.json node --require ts-node/register --test test/*.test.ts test/**/*.test.ts",
    +    "test:recipes": "TS_NODE_PROJECT=tsconfig.recipes.json node --require ts-node/register --test recipes/**/*.test.ts",
    +    "pretest:all": "npm run lint",
    +    "test:all": "TS_NODE_PROJECT=tsconfig.ts-node.json node --require ts-node/register --test test/*.test.ts test/**/*.test.ts recipes/**/*.test.ts",
    +    "test:coverage": "c8 npm run test:all",
    +    "ts:check": "tsc --noEmit --project tsconfig.typecheck.json",
    +    "prebuild": "rimraf dist",
    +    "build": "tsup",
    +    "prepublishOnly": "npm run build"
       }
     }
    
  • .prettierignore+29 0 added
    @@ -0,0 +1,29 @@
    +# Dependencies
    +node_modules/
    +
    +# Build outputs
    +dist/
    +coverage/
    +
    +# Lock files
    +yarn.lock
    +package-lock.json
    +
    +# Logs
    +*.log
    +
    +# OS files
    +.DS_Store
    +Thumbs.db
    +
    +# IDE
    +.vscode/
    +.idea/
    +
    +# Git
    +.git/
    +
    +# Temporary files
    +*.tmp
    +*.temp
    +
    
  • .prettierrc.js+0 5 removed
    @@ -1,5 +0,0 @@
    -module.exports = {
    -  singleQuote: true,
    -  bracketSpacing: true,
    -  trailingComma: 'none'
    -};
    
  • .prettierrc.json+5 0 added
    @@ -0,0 +1,5 @@
    +{
    +  "singleQuote": true,
    +  "bracketSpacing": true,
    +  "trailingComma": "none"
    +}
    
  • README.md+1136 43 modified
    @@ -1,69 +1,1166 @@
     # [@koa/router](https://github.com/koajs/router)
     
    -> Router middleware for [Koa](https://github.com/koajs/koa). Maintained by [Forward Email][forward-email] and [Lad][].
    +> Modern TypeScript router middleware for [Koa](https://github.com/koajs/koa). Maintained by [Forward Email][forward-email] and [Lad][].
     
     [![build status](https://github.com/koajs/router/actions/workflows/ci.yml/badge.svg)](https://github.com/koajs/router/actions/workflows/ci.yml)
    -
    -<!-- [![code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) -->
    -
     [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
     [![made with lass](https://img.shields.io/badge/made_with-lass-95CC28.svg)](https://lass.js.org)
     [![license](https://img.shields.io/github/license/koajs/router.svg)](LICENSE)
     
    -
     ## Table of Contents
     
    -* [Features](#features)
    -* [Migrating to 7 / Koa 2](#migrating-to-7--koa-2)
    -* [Install](#install)
    -* [Typescript Support](#typescript-support)
    -* [API Reference](#api-reference)
    -* [Contributors](#contributors)
    -* [License](#license)
    -
    +- [Features](#features)
    +- [Installation](#installation)
    +- [TypeScript Support](#typescript-support)
    +- [Quick Start](#quick-start)
    +- [API Documentation](#api-documentation)
    +- [Advanced Features](#advanced-features)
    +- [Best Practices](#best-practices)
    +- [Recipes](#recipes)
    +- [Performance](#performance)
    +- [Testing](#testing)
    +- [Migration Guides](#migration-guides)
    +- [Contributing](#contributing)
    +- [License](#license)
    +- [Contributors](#contributors)
     
     ## Features
     
    -* Express-style routing (`app.get`, `app.put`, `app.post`, etc.)
    -* Named URL parameters
    -* Named routes with URL generation
    -* Match routes with specific host
    -* Responds to `OPTIONS` requests with allowed methods
    -* Support for `405 Method Not Allowed` and `501 Not Implemented`
    -* Multiple route middleware
    -* Multiple and nestable routers
    -* `async/await` support
    +- ✅ **Full TypeScript Support** - Written in TypeScript with comprehensive type definitions
    +- ✅ **Express-Style Routing** - Familiar `app.get`, `app.post`, `app.put`, etc.
    +- ✅ **Named URL Parameters** - Extract parameters from URLs
    +- ✅ **Named Routes** - Generate URLs from route names
    +- ✅ **Host Matching** - Match routes based on hostname
    +- ✅ **HEAD Request Support** - Automatic HEAD support for GET routes
    +- ✅ **Multiple Middleware** - Chain multiple middleware functions
    +- ✅ **Nested Routers** - Mount routers within routers
    +- ✅ **RegExp Paths** - Use regular expressions for flexible path matching
    +- ✅ **Parameter Middleware** - Run middleware for specific URL parameters
    +- ✅ **Path-to-RegExp v8** - Modern, predictable path matching
    +- ✅ **405 Method Not Allowed** - Automatic method validation
    +- ✅ **501 Not Implemented** - Proper HTTP status codes
    +- ✅ **Async/Await** - Full promise-based middleware support
     
    +## Installation
     
    -## Migrating to 7 / Koa 2
    +**npm:**
     
    -* The API has changed to match the new promise-based middleware
    -  signature of koa 2. See the [koa 2.x readme](https://github.com/koajs/koa/tree/2.0.0-alpha.3) for more
    -  information.
    -* Middleware is now always run in the order declared by `.use()` (or `.get()`,
    -  etc.), which matches Express 4 API.
    +```bash
    +npm install @koa/router
    +```
     
    +**yarn:**
     
    -## Install
    +```bash
    +yarn add @koa/router
    +```
     
    -[npm][]:
    +**Requirements:**
     
    -```sh
    -npm install @koa/router
    +- Node.js >= 20 (tested on v20, v22, v24, v25)
    +- Koa >= 2.0.0
    +
    +## TypeScript Support
    +
    +@koa/router is written in TypeScript and includes comprehensive type definitions out of the box. No need for `@types/*` packages!
    +
    +### Basic Usage
    +
    +```typescript
    +import Router, { RouterContext } from '@koa/router';
    +
    +const router = new Router();
    +
    +// Fully typed context
    +router.get('/:id', (ctx: RouterContext, next) => {
    +  const id = ctx.params.id; // Type-safe parameters
    +  ctx.body = { id };
    +});
     ```
     
    +### Generic Types
    +
    +The router supports generic type parameters for full type safety with custom state and context types:
    +
    +```typescript
    +import Router, { RouterContext } from '@koa/router';
    +import type { Next } from 'koa';
    +
    +// Define your application state
    +interface AppState {
    +  user?: {
    +    id: string;
    +    email: string;
    +  };
    +}
     
    -## Typescript Support
    +// Define your custom context
    +interface AppContext {
    +  requestId: string;
    +}
     
    -```sh
    -npm install --save-dev @types/koa__router
    +// Create router with generics
    +const router = new Router<AppState, AppContext>();
    +
    +// Type-safe route handlers
    +router.get(
    +  '/profile',
    +  (ctx: RouterContext<AppState, AppContext>, next: Next) => {
    +    // ctx.state.user is fully typed
    +    if (ctx.state.user) {
    +      ctx.body = {
    +        user: ctx.state.user,
    +        requestId: ctx.requestId // Custom context property
    +      };
    +    }
    +  }
    +);
     ```
     
    +### Extending Types in Route Handlers
    +
    +HTTP methods support generic type parameters to extend state and context types:
    +
    +```typescript
    +interface UserState {
    +  user: { id: string; name: string };
    +}
     
    -## API Reference
    +interface UserContext {
    +  permissions: string[];
    +}
    +
    +// Extend types for specific routes
    +router.get<UserState, UserContext>(
    +  '/users/:id',
    +  async (ctx: RouterContext<UserState, UserContext>) => {
    +    // ctx.state.user is fully typed
    +    // ctx.permissions is fully typed
    +    ctx.body = {
    +      user: ctx.state.user,
    +      permissions: ctx.permissions
    +    };
    +  }
    +);
    +```
    +
    +### Parameter Middleware Types
    +
    +```typescript
    +import type { RouterParameterMiddleware } from '@koa/router';
    +import type { Next } from 'koa';
    +
    +// Type-safe parameter middleware
    +router.param('id', ((value: string, ctx: RouterContext, next: Next) => {
    +  if (!/^\d+$/.test(value)) {
    +    ctx.throw(400, 'Invalid ID format');
    +  }
    +  return next();
    +}) as RouterParameterMiddleware);
    +```
    +
    +### Available Types
    +
    +```typescript
    +import {
    +  Router,
    +  RouterContext,
    +  RouterOptions,
    +  RouterMiddleware,
    +  RouterParameterMiddleware,
    +  RouterParamContext,
    +  AllowedMethodsOptions,
    +  UrlOptions,
    +  HttpMethod
    +} from '@koa/router';
    +import type { Next } from 'koa';
    +
    +// Router with generics
    +type MyRouter = Router<AppState, AppContext>;
    +
    +// Context with generics
    +type MyContext = RouterContext<AppState, AppContext, BodyType>;
    +
    +// Middleware with generics
    +type MyMiddleware = RouterMiddleware<AppState, AppContext, BodyType>;
    +
    +// Parameter middleware with generics
    +type MyParamMiddleware = RouterParameterMiddleware<
    +  AppState,
    +  AppContext,
    +  BodyType
    +>;
    +```
    +
    +### Type Safety Features
    +
    +- ✅ **Full generic support** - `Router<StateT, ContextT>` for custom state and context types
    +- ✅ **Type-safe parameters** - `ctx.params` is fully typed
    +- ✅ **Type-safe state** - `ctx.state` respects your state type
    +- ✅ **Type-safe middleware** - Middleware functions are fully typed
    +- ✅ **Type-safe HTTP methods** - Methods support generic type extensions
    +- ✅ **Compatible with @types/koa-router** - Matches official type structure
    +
    +## Quick Start
    +
    +```javascript
    +import Koa from 'koa';
    +import Router from '@koa/router';
    +
    +const app = new Koa();
    +const router = new Router();
    +
    +// Define routes
    +router.get('/', (ctx, next) => {
    +  ctx.body = 'Hello World!';
    +});
    +
    +router.get('/users/:id', (ctx, next) => {
    +  ctx.body = { id: ctx.params.id };
    +});
    +
    +// Apply router middleware
    +app.use(router.routes()).use(router.allowedMethods());
    +
    +app.listen(3000);
    +```
     
    -See [API Reference](./API.md) for more documentation.
    +## API Documentation
    +
    +### Router Constructor
    +
    +**`new Router([options])`**
    +
    +Create a new router instance.
    +
    +**Options:**
    +
    +| Option      | Type                           | Description                               |
    +| ----------- | ------------------------------ | ----------------------------------------- |
    +| `prefix`    | `string`                       | Prefix all routes with this path          |
    +| `exclusive` | `boolean`                      | Only run the most specific matching route |
    +| `host`      | `string \| string[] \| RegExp` | Match routes only for this hostname(s)    |
    +| `methods`   | `string[]`                     | Custom HTTP methods to support            |
    +| `sensitive` | `boolean`                      | Enable case-sensitive routing             |
    +| `strict`    | `boolean`                      | Require trailing slashes                  |
    +
    +**Example:**
    +
    +```javascript
    +const router = new Router({
    +  prefix: '/api',
    +  exclusive: true,
    +  host: 'example.com'
    +});
    +```
    +
    +### HTTP Methods
    +
    +Router provides methods for all standard HTTP verbs:
    +
    +- `router.get(path, ...middleware)`
    +- `router.post(path, ...middleware)`
    +- `router.put(path, ...middleware)`
    +- `router.patch(path, ...middleware)`
    +- `router.delete(path, ...middleware)` or `router.del(path, ...middleware)`
    +- `router.head(path, ...middleware)`
    +- `router.options(path, ...middleware)`
    +- `router.connect(path, ...middleware)` - CONNECT method
    +- `router.trace(path, ...middleware)` - TRACE method
    +- `router.all(path, ...middleware)` - Match any HTTP method
    +
    +**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.
    +
    +**Basic Example:**
    +
    +```javascript
    +router
    +  .get('/users', getUsers)
    +  .post('/users', createUser)
    +  .put('/users/:id', updateUser)
    +  .delete('/users/:id', deleteUser)
    +  .all('/users/:id', logAccess); // Runs for any method
    +```
    +
    +**Using Less Common HTTP Methods:**
    +
    +All standard HTTP methods from Node.js are automatically available. Here's an example using `PATCH` and `PURGE`:
    +
    +```javascript
    +const router = new Router();
    +
    +// PATCH method (standard HTTP method for partial updates)
    +router.patch('/users/:id', async (ctx) => {
    +  // Partial update
    +  ctx.body = { message: 'User partially updated' };
    +});
    +
    +// PURGE method (standard HTTP method, commonly used for cache invalidation)
    +router.purge('/cache/:key', async (ctx) => {
    +  // Clear cache
    +  await clearCache(ctx.params.key);
    +  ctx.body = { message: 'Cache cleared' };
    +});
    +
    +// COPY method (standard HTTP method)
    +router.copy('/files/:source', async (ctx) => {
    +  await copyFile(ctx.params.source, ctx.request.body.destination);
    +  ctx.body = { message: 'File copied' };
    +});
    +
    +// Limiting which methods the router responds to
    +const apiRouter = new Router({
    +  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] // Only these methods
    +});
    +
    +apiRouter.get('/users', getUsers);
    +apiRouter.post('/users', createUser);
    +// router.purge() won't work here because PURGE is not in the methods array
    +```
    +
    +**Using Less Common HTTP Methods:**
    +
    +All standard HTTP methods from Node.js are automatically available. Here's an example using `PATCH` and `PURGE`:
    +
    +```javascript
    +const router = new Router();
    +
    +// PATCH method (standard HTTP method for partial updates)
    +router.patch('/users/:id', async (ctx) => {
    +  // Partial update
    +  ctx.body = { message: 'User partially updated' };
    +});
    +
    +// PURGE method (standard HTTP method, commonly used for cache invalidation)
    +router.purge('/cache/:key', async (ctx) => {
    +  // Clear cache
    +  await clearCache(ctx.params.key);
    +  ctx.body = { message: 'Cache cleared' };
    +});
    +
    +// COPY method (standard HTTP method)
    +router.copy('/files/:source', async (ctx) => {
    +  await copyFile(ctx.params.source, ctx.request.body.destination);
    +  ctx.body = { message: 'File copied' };
    +});
    +
    +// Limiting which methods the router responds to
    +const apiRouter = new Router({
    +  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] // Only these methods
    +});
    +
    +apiRouter.get('/users', getUsers);
    +apiRouter.post('/users', createUser);
    +// router.purge() won't work here because PURGE is not in the methods array
    +```
    +
    +**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.
    +
    +### Named Routes
    +
    +Routes can be named for URL generation:
    +
    +```javascript
    +router.get('user', '/users/:id', (ctx) => {
    +  ctx.body = { id: ctx.params.id };
    +});
    +
    +// Generate URL
    +router.url('user', 3);
    +// => "/users/3"
    +
    +router.url('user', { id: 3 });
    +// => "/users/3"
    +
    +// With query parameters
    +router.url('user', { id: 3 }, { query: { limit: 10 } });
    +// => "/users/3?limit=10"
    +
    +// In middleware
    +router.use((ctx, next) => {
    +  ctx.redirect(ctx.router.url('user', 1));
    +});
    +```
     
    +### Multiple Middleware
    +
    +Chain multiple middleware functions for a single route:
    +
    +```javascript
    +router.get(
    +  '/users/:id',
    +  async (ctx, next) => {
    +    // Load user from database
    +    ctx.state.user = await User.findById(ctx.params.id);
    +    return next();
    +  },
    +  async (ctx, next) => {
    +    // Check permissions
    +    if (!ctx.state.user) {
    +      ctx.throw(404, 'User not found');
    +    }
    +    return next();
    +  },
    +  (ctx) => {
    +    // Send response
    +    ctx.body = ctx.state.user;
    +  }
    +);
    +```
    +
    +### Nested Routers
    +
    +Mount routers within routers:
    +
    +```javascript
    +const usersRouter = new Router();
    +usersRouter.get('/', getUsers);
    +usersRouter.get('/:id', getUser);
    +
    +const postsRouter = new Router();
    +postsRouter.get('/', getPosts);
    +postsRouter.get('/:id', getPost);
    +
    +const apiRouter = new Router({ prefix: '/api' });
    +apiRouter.use('/users', usersRouter.routes());
    +apiRouter.use('/posts', postsRouter.routes());
    +
    +app.use(apiRouter.routes());
    +```
    +
    +**Note:** Parameters from parent routes are properly propagated to nested router middleware and handlers.
    +
    +### Router Prefixes
    +
    +Set a prefix for all routes in a router:
    +
    +**Option 1: In constructor**
    +
    +```javascript
    +const router = new Router({ prefix: '/api' });
    +router.get('/users', handler); // Responds to /api/users
    +```
    +
    +**Option 2: Using .prefix()**
    +
    +```javascript
    +const router = new Router();
    +router.prefix('/api');
    +router.get('/users', handler); // Responds to /api/users
    +```
    +
    +**With parameters:**
    +
    +```javascript
    +const router = new Router({ prefix: '/api/v:version' });
    +router.get('/users', (ctx) => {
    +  ctx.body = {
    +    version: ctx.params.version,
    +    users: []
    +  };
    +});
    +// Responds to /api/v1/users, /api/v2/users, etc.
    +```
    +
    +**Note:** Middleware now correctly executes when the prefix contains parameters.
    +
    +### URL Parameters
    +
    +Named parameters are captured and available at `ctx.params`:
    +
    +```javascript
    +router.get('/:category/:title', (ctx) => {
    +  console.log(ctx.params);
    +  // => { category: 'programming', title: 'how-to-node' }
    +
    +  ctx.body = {
    +    category: ctx.params.category,
    +    title: ctx.params.title
    +  };
    +});
    +```
    +
    +**Optional parameters:**
    +
    +```javascript
    +router.get('/user{/:id}?', (ctx) => {
    +  // Matches both /user and /user/123
    +  ctx.body = { id: ctx.params.id || 'all' };
    +});
    +```
    +
    +**Wildcard parameters:**
    +
    +```javascript
    +router.get('/files/{/*path}', (ctx) => {
    +  // Matches /files/a/b/c.txt
    +  ctx.body = { path: ctx.params.path }; // => a/b/c.txt
    +});
    +```
    +
    +**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.
    +
    +### router.routes()
    +
    +Returns router middleware which dispatches matched routes.
    +
    +```javascript
    +app.use(router.routes());
    +```
    +
    +### router.use()
    +
    +Use middleware, **if and only if**, a route is matched.
    +
    +**Signature:**
    +
    +```javascript
    +router.use([path], ...middleware);
    +```
    +
    +**Examples:**
    +
    +```javascript
    +// Run for all matched routes
    +router.use(session());
    +
    +// Run only for specific path
    +router.use('/admin', requireAuth());
    +
    +// Run for multiple paths
    +router.use(['/admin', '/dashboard'], requireAuth());
    +
    +// Run for RegExp paths
    +router.use(/^\/api\//, apiAuth());
    +
    +// Mount nested routers
    +const nestedRouter = new Router();
    +router.use('/nested', nestedRouter.routes());
    +```
    +
    +**Note:** Middleware path boundaries are correctly enforced. Middleware scoped to `/api` will only run for routes matching `/api/*`, not for unrelated routes.
    +
    +### router.prefix()
    +
    +Set the path prefix for a Router instance after initialization.
    +
    +```javascript
    +const router = new Router();
    +router.get('/', handler); // Responds to /
    +
    +router.prefix('/api');
    +router.get('/', handler); // Now responds to /api
    +```
    +
    +### router.allowedMethods()
    +
    +Returns middleware for responding to `OPTIONS` requests with allowed methods,
    +and `405 Method Not Allowed` / `501 Not Implemented` responses.
    +
    +**Options:**
    +
    +| Option             | Type       | Description                              |
    +| ------------------ | ---------- | ---------------------------------------- |
    +| `throw`            | `boolean`  | Throw errors instead of setting response |
    +| `notImplemented`   | `function` | Custom function for 501 errors           |
    +| `methodNotAllowed` | `function` | Custom function for 405 errors           |
    +
    +**Example:**
    +
    +```javascript
    +app.use(router.routes());
    +app.use(router.allowedMethods());
    +```
    +
    +**With custom error handling:**
    +
    +```javascript
    +app.use(
    +  router.allowedMethods({
    +    throw: true,
    +    notImplemented: () => new Error('Not Implemented'),
    +    methodNotAllowed: () => new Error('Method Not Allowed')
    +  })
    +);
    +```
    +
    +### router.redirect()
    +
    +Redirect `source` to `destination` URL with optional status code.
    +
    +```javascript
    +router.redirect('/login', 'sign-in', 301);
    +router.redirect('/old-path', '/new-path');
    +
    +// Redirect to named route
    +router.get('home', '/', handler);
    +router.redirect('/index', 'home');
    +```
    +
    +### router.route()
    +
    +Lookup a route by name.
    +
    +```javascript
    +const layer = router.route('user');
    +if (layer) {
    +  console.log(layer.path); // => /users/:id
    +}
    +```
    +
    +### router.url()
    +
    +Generate URL from route name and parameters.
    +
    +```javascript
    +router.get('user', '/users/:id', handler);
    +
    +router.url('user', 3);
    +// => "/users/3"
    +
    +router.url('user', { id: 3 });
    +// => "/users/3"
    +
    +router.url('user', { id: 3 }, { query: { limit: 1 } });
    +// => "/users/3?limit=1"
    +
    +router.url('user', { id: 3 }, { query: 'limit=1' });
    +// => "/users/3?limit=1"
    +```
    +
    +**In middleware:**
    +
    +```javascript
    +router.use((ctx, next) => {
    +  // Access router instance via ctx.router
    +  const userUrl = ctx.router.url('user', ctx.state.userId);
    +  ctx.redirect(userUrl);
    +  return next();
    +});
    +```
    +
    +### router.param()
    +
    +Run middleware for named route parameters.
    +
    +**Signature:**
    +
    +```typescript
    +router.param(param: string, middleware: RouterParameterMiddleware): Router
    +```
    +
    +**TypeScript Example:**
    +
    +```typescript
    +import type { RouterParameterMiddleware } from '@koa/router';
    +import type { Next } from 'koa';
    +
    +router.param('user', (async (id: string, ctx: RouterContext, next: Next) => {
    +  ctx.state.user = await User.findById(id);
    +  if (!ctx.state.user) {
    +    ctx.throw(404, 'User not found');
    +  }
    +  return next();
    +}) as RouterParameterMiddleware);
    +
    +router.get('/users/:user', (ctx: RouterContext) => {
    +  // ctx.state.user is already loaded and typed
    +  ctx.body = ctx.state.user;
    +});
    +
    +router.get('/users/:user/friends', (ctx: RouterContext) => {
    +  // ctx.state.user is available here too
    +  return ctx.state.user.getFriends();
    +});
    +```
    +
    +**JavaScript Example:**
    +
    +```javascript
    +router
    +  .param('user', async (id, ctx, next) => {
    +    ctx.state.user = await User.findById(id);
    +    if (!ctx.state.user) {
    +      ctx.throw(404, 'User not found');
    +    }
    +    return next();
    +  })
    +  .get('/users/:user', (ctx) => {
    +    // ctx.state.user is already loaded
    +    ctx.body = ctx.state.user;
    +  })
    +  .get('/users/:user/friends', (ctx) => {
    +    // ctx.state.user is available here too
    +    return ctx.state.user.getFriends();
    +  });
    +```
    +
    +**Multiple param handlers:**
    +
    +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):
    +
    +```javascript
    +router
    +  .param('id', validateIdFormat)
    +  .param('id', checkIdExists)
    +  .param('id', checkPermissions)
    +  .get('/resource/:id', handler);
    +// All three param handlers run once per request
    +```
    +
    +### Router.url() (static)
    +
    +Generate URL from path pattern and parameters (static method).
    +
    +```javascript
    +const url = Router.url('/users/:id', { id: 1 });
    +// => "/users/1"
    +
    +const url = Router.url('/users/:id', { id: 1, name: 'John' });
    +// => "/users/1"
    +```
    +
    +## Advanced Features
    +
    +### Host Matching
    +
    +Match routes only for specific hostnames:
    +
    +```javascript
    +// Exact match with single host
    +const routerA = new Router({
    +  host: 'example.com'
    +});
    +
    +// Match multiple hosts with array
    +const routerB = new Router({
    +  host: ['some-domain.com', 'www.some-domain.com', 'some.other-domain.com']
    +});
    +
    +// Match patterns with RegExp
    +const routerC = new Router({
    +  host: /^(.*\.)?example\.com$/ // Match all subdomains
    +});
    +```
    +
    +**Host Matching Options:**
    +
    +- `string` - Exact match (case-sensitive)
    +- `string[]` - Matches if the request host equals any string in the array
    +- `RegExp` - Pattern match using regular expression
    +- `undefined` - Matches all hosts (default)
    +
    +### Regular Expressions
    +
    +Use RegExp for flexible path matching:
    +
    +**Full RegExp routes:**
    +
    +```javascript
    +router.get(/^\/users\/(\d+)$/, (ctx) => {
    +  const id = ctx.params[0]; // First capture group
    +  ctx.body = { id };
    +});
    +```
    +
    +**RegExp in router.use():**
    +
    +```javascript
    +router.use(/^\/api\//, apiMiddleware);
    +router.use(/^\/admin\//, adminAuth);
    +```
    +
    +### Parameter Validation
    +
    +Validate parameters using middleware or handlers:
    +
    +**Option 1: In Handler**
    +
    +```javascript
    +router.get('/user/:id', (ctx) => {
    +  if (!/^\d+$/.test(ctx.params.id)) {
    +    ctx.throw(400, 'Invalid ID format');
    +  }
    +
    +  ctx.body = { id: parseInt(ctx.params.id, 10) };
    +});
    +```
    +
    +**Option 2: Middleware**
    +
    +```javascript
    +function validateUUID(paramName) {
    +  const uuidRegex =
    +    /^[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}$/;
    +
    +  return async (ctx, next) => {
    +    if (!uuidRegex.test(ctx.params[paramName])) {
    +      ctx.throw(400, `Invalid ${paramName} format`);
    +    }
    +    await next();
    +  };
    +}
    +
    +router.get('/user/:id', validateUUID('id'), handler);
    +```
    +
    +**Option 3: router.param()**
    +
    +```javascript
    +router.param('id', (value, ctx, next) => {
    +  if (!/^\d+$/.test(value)) {
    +    ctx.throw(400, 'Invalid ID');
    +  }
    +  ctx.params.id = parseInt(value, 10); // Convert to number
    +  return next();
    +});
    +
    +router.get('/user/:id', handler);
    +router.get('/post/:id', handler);
    +// Both routes validate :id parameter
    +```
    +
    +### Catch-All Routes
    +
    +Create a catch-all route that only runs when no other routes match:
    +
    +```javascript
    +router.get('/users', handler1);
    +router.get('/posts', handler2);
    +
    +// Catch-all for unmatched routes
    +router.all('{/*rest}', (ctx) => {
    +  if (!ctx.matched || ctx.matched.length === 0) {
    +    ctx.status = 404;
    +    ctx.body = { error: 'Not Found' };
    +  }
    +});
    +```
    +
    +### Array of Paths
    +
    +Register multiple paths with the same middleware:
    +
    +```javascript
    +router.get(['/users', '/people'], handler);
    +// Responds to both /users and /people
    +```
    +
    +### 404 Handling
    +
    +Implement custom 404 handling:
    +
    +```javascript
    +app.use(router.routes());
    +
    +// 404 middleware - runs after router
    +app.use((ctx) => {
    +  if (!ctx.matched || ctx.matched.length === 0) {
    +    ctx.status = 404;
    +    ctx.body = {
    +      error: 'Not Found',
    +      path: ctx.path
    +    };
    +  }
    +});
    +```
    +
    +## Best Practices
    +
    +### 1. Use Middleware Composition
    +
    +```javascript
    +// ✅ Good: Compose reusable middleware
    +const requireAuth = () => async (ctx, next) => {
    +  if (!ctx.state.user) ctx.throw(401);
    +  await next();
    +};
    +
    +const requireAdmin = () => async (ctx, next) => {
    +  if (!ctx.state.user.isAdmin) ctx.throw(403);
    +  await next();
    +};
    +
    +router.get('/admin', requireAuth(), requireAdmin(), adminHandler);
    +```
    +
    +### 2. Organize Routes by Resource
    +
    +```javascript
    +// ✅ Good: Group related routes
    +const usersRouter = new Router({ prefix: '/users' });
    +usersRouter.get('/', listUsers);
    +usersRouter.post('/', createUser);
    +usersRouter.get('/:id', getUser);
    +usersRouter.put('/:id', updateUser);
    +usersRouter.delete('/:id', deleteUser);
    +
    +app.use(usersRouter.routes());
    +```
    +
    +### 3. Use Named Routes
    +
    +```javascript
    +// ✅ Good: Name important routes
    +router.get('home', '/', homeHandler);
    +router.get('user-profile', '/users/:id', profileHandler);
    +
    +// Easy to generate URLs
    +ctx.redirect(ctx.router.url('home'));
    +ctx.redirect(ctx.router.url('user-profile', ctx.state.user.id));
    +```
    +
    +### 4. Validate Early
    +
    +```javascript
    +// ✅ Good: Validate at the route level
    +router
    +  .param('id', validateId)
    +  .get('/users/:id', getUser)
    +  .put('/users/:id', updateUser)
    +  .delete('/users/:id', deleteUser);
    +// Validation runs once for all routes
    +```
    +
    +### 5. Handle Errors Consistently
    +
    +```javascript
    +// ✅ Good: Centralized error handling
    +app.use(async (ctx, next) => {
    +  try {
    +    await next();
    +  } catch (err) {
    +    ctx.status = err.status || 500;
    +    ctx.body = {
    +      error: err.message,
    +      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    +    };
    +  }
    +});
    +
    +app.use(router.routes());
    +app.use(router.allowedMethods({ throw: true }));
    +```
    +
    +### 6. Access Router Context Properties
    +
    +The router adds useful properties to the Koa context:
    +
    +```typescript
    +router.get('/users/:id', (ctx: RouterContext) => {
    +  // URL parameters (fully typed)
    +  const id = ctx.params.id; // string
    +
    +  // Router instance
    +  const router = ctx.router;
    +
    +  // Matched route path
    +  const routePath = ctx.routerPath; // => '/users/:id'
    +
    +  // Matched route name (if named)
    +  const routeName = ctx.routerName; // => 'user' (if named)
    +
    +  // All matched layers
    +  const matched = ctx.matched; // Array of Layer objects
    +
    +  // Captured values from RegExp routes
    +  const captures = ctx.captures; // string[] | undefined
    +
    +  // Generate URLs
    +  const url = ctx.router.url('user', id);
    +
    +  ctx.body = { id, routePath, routeName, url };
    +});
    +```
    +
    +### 7. Type-Safe Context Extensions
    +
    +Extend the router context with custom properties:
    +
    +```typescript
    +import Router, { RouterContext } from '@koa/router';
    +import type { Next } from 'koa';
    +
    +interface UserState {
    +  user?: { id: string; email: string };
    +}
    +
    +interface CustomContext {
    +  requestId: string;
    +  startTime: number;
    +}
    +
    +const router = new Router<UserState, CustomContext>();
    +
    +// Middleware that adds to context
    +router.use(async (ctx: RouterContext<UserState, CustomContext>, next: Next) => {
    +  ctx.requestId = crypto.randomUUID();
    +  ctx.startTime = Date.now();
    +  await next();
    +});
    +
    +router.get(
    +  '/users/:id',
    +  async (ctx: RouterContext<UserState, CustomContext>) => {
    +    // All properties are fully typed
    +    ctx.body = {
    +      user: ctx.state.user,
    +      requestId: ctx.requestId,
    +      duration: Date.now() - ctx.startTime
    +    };
    +  }
    +);
    +```
    +
    +## Recipes
    +
    +Common patterns and recipes for building real-world applications with @koa/router.
    +
    +See the [recipes directory](./recipes/) for complete TypeScript examples:
    +
    +- **[Nested Routes](./recipes/nested-routes/)** - Production-ready nested router patterns with multiple levels (3-4 levels deep), parameter propagation, and real-world examples
    +- **[RESTful API Structure](./recipes/restful-api-structure/)** - Organize your API with nested routers
    +- **[Authentication & Authorization](./recipes/authentication-authorization/)** - JWT-based authentication with middleware
    +- **[Request Validation](./recipes/request-validation/)** - Validate request data with middleware
    +- **[Parameter Validation](./recipes/parameter-validation/)** - Validate and transform parameters using router.param()
    +- **[API Versioning](./recipes/api-versioning/)** - Implement API versioning with multiple routers
    +- **[Error Handling](./recipes/error-handling/)** - Centralized error handling with custom error classes
    +- **[Pagination](./recipes/pagination/)** - Implement pagination for list endpoints
    +- **[Health Checks](./recipes/health-checks/)** - Add health check endpoints for monitoring
    +- **[TypeScript Recipe](./recipes/typescript-recipe/)** - Full TypeScript example with types and type safety
    +
    +Each recipe file contains complete, runnable TypeScript code that you can copy and adapt to your needs.
    +
    +## Performance
    +
    +@koa/router is designed for high performance:
    +
    +- **Fast path matching** with path-to-regexp v8
    +- **Efficient RegExp compilation** and caching
    +- **Minimal overhead** - zero runtime type checking
    +- **Optimized middleware execution** with koa-compose
    +
    +**Benchmarks:**
    +
    +```bash
    +# Run benchmarks
    +yarn benchmark
    +
    +# Run all benchmark scenarios
    +yarn benchmark:all
    +```
    +
    +## Testing
    +
    +@koa/router uses Node.js native test runner:
    +
    +```bash
    +# Run all tests (core + recipes)
    +yarn test:all
    +
    +# Run core tests only
    +yarn test:core
    +
    +# Run recipe tests only
    +yarn test:recipes
    +
    +# Run tests with coverage
    +yarn test:coverage
    +
    +# Type check
    +yarn ts:check
    +
    +# Format code with Prettier
    +yarn format
    +
    +# Check code formatting
    +yarn format:check
    +
    +# Lint code
    +yarn lint
    +```
    +
    +**Example test:**
    +
    +```javascript
    +import { describe, it } from 'node:test';
    +import assert from 'node:assert';
    +import Koa from 'koa';
    +import Router from '@koa/router';
    +import request from 'supertest';
    +
    +describe('Router', () => {
    +  it('should route GET requests', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.get('/users', (ctx) => {
    +      ctx.body = { users: [] };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res = await request(app.callback()).get('/users').expect(200);
    +
    +    assert.deepStrictEqual(res.body, { users: [] });
    +  });
    +});
    +```
    +
    +## Migration Guides
    +
    +**Breaking Changes:**
    +
    +- Custom regex patterns in parameters (`:param(regex)`) are **no longer supported** due to path-to-regexp v8. Use validation in handlers or middleware instead.
    +- Node.js >= 20 is required.
    +- TypeScript types are now included in the package (no need for `@types/@koa/router`).
    +
    +**Upgrading:**
    +
    +1. Update Node.js to >= 20
    +2. Replace custom regex parameters with validation middleware
    +3. Remove `@types/@koa/router` if installed (types are now included)
    +4. Update any code using deprecated features
    +
    +**Backward Compatibility:**
    +
    +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!
    +
    +## Contributing
    +
    +Contributions are welcome!
    +
    +### Development Setup
    +
    +```bash
    +# Clone repository
    +git clone https://github.com/koajs/router.git
    +cd router
    +
    +# Install dependencies (using yarn)
    +yarn install
    +
    +# Run tests
    +yarn test:all
    +
    +# Run tests with coverage
    +yarn test:coverage
    +
    +# Format code
    +yarn format
    +
    +# Check formatting
    +yarn format:check
    +
    +# Lint code
    +yarn lint
    +
    +# Build TypeScript
    +yarn build
    +
    +# Type check
    +yarn ts:check
    +```
     
     ## Contributors
     
    @@ -73,16 +1170,12 @@ See [API Reference](./API.md) for more documentation.
     | **@koajs**       |
     | **Imed Jaberi**  |
     
    -
     ## License
     
    -[MIT](LICENSE) © Alex Mingoia
    -
    +[MIT](LICENSE) © Koa.js
     
    -##
    +---
     
     [forward-email]: https://forwardemail.net
    -
     [lad]: https://lad.js.org
    -
     [npm]: https://www.npmjs.com
    
  • recipes/api-versioning/api-versioning.test.ts+83 0 added
    @@ -0,0 +1,83 @@
    +/**
    + * Tests for API Versioning Recipe
    + */
    +
    +import { describe, it } from 'node:test';
    +import * as assert from 'node:assert';
    +import * as http from 'node:http';
    +import Router from '../router-module-loader';
    +import request from 'supertest';
    +import Koa from 'koa';
    +
    +describe('API Versioning', () => {
    +  it('should support multiple API versions', async () => {
    +    const app = new Koa();
    +
    +    const getUsersV1 = async (ctx: any) => {
    +      ctx.body = { users: [{ id: 1 }], version: 'v1' };
    +    };
    +
    +    const getUserV1 = async (ctx: any) => {
    +      ctx.body = { user: { id: ctx.params.id }, version: 'v1' };
    +    };
    +
    +    const getUsersV2 = async (ctx: any) => {
    +      ctx.body = {
    +        users: [{ id: 1 }],
    +        version: 'v2',
    +        metadata: { count: 1, timestamp: new Date().toISOString() }
    +      };
    +    };
    +
    +    const getUserV2 = async (ctx: any) => {
    +      ctx.body = {
    +        user: { id: ctx.params.id },
    +        version: 'v2',
    +        links: { self: `/api/v2/users/${ctx.params.id}` }
    +      };
    +    };
    +
    +    const v1Router = new Router({ prefix: '/v1' });
    +    v1Router.get('/users', getUsersV1);
    +    v1Router.get('/users/:id', getUserV1);
    +
    +    const v2Router = new Router({ prefix: '/v2' });
    +    v2Router.get('/users', getUsersV2);
    +    v2Router.get('/users/:id', getUserV2);
    +
    +    const apiRouter = new Router({ prefix: '/api' });
    +    apiRouter.use(v1Router.routes(), v1Router.allowedMethods());
    +    apiRouter.use(v2Router.routes(), v2Router.allowedMethods());
    +
    +    app.use(apiRouter.routes());
    +    app.use(apiRouter.allowedMethods());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/api/v1/users')
    +      .expect(200);
    +
    +    assert.strictEqual(res1.body.version, 'v1');
    +    assert.strictEqual(Array.isArray(res1.body.users), true);
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/api/v1/users/123')
    +      .expect(200);
    +
    +    assert.strictEqual(res2.body.version, 'v1');
    +    assert.strictEqual(res2.body.user.id, '123');
    +
    +    const res3 = await request(http.createServer(app.callback()))
    +      .get('/api/v2/users')
    +      .expect(200);
    +
    +    assert.strictEqual(res3.body.version, 'v2');
    +    assert.strictEqual(res3.body.metadata.count, 1);
    +
    +    const res4 = await request(http.createServer(app.callback()))
    +      .get('/api/v2/users/456')
    +      .expect(200);
    +
    +    assert.strictEqual(res4.body.version, 'v2');
    +    assert.strictEqual(res4.body.links.self, '/api/v2/users/456');
    +  });
    +});
    
  • recipes/api-versioning/api-versioning.ts+52 0 added
    @@ -0,0 +1,52 @@
    +/**
    + * API Versioning Recipe
    + *
    + * Implement API versioning with multiple routers.
    + *
    + * This example shows how to maintain multiple API versions
    + * simultaneously with separate routers.
    + */
    +import Koa from 'koa';
    +import Router from '../router-module-loader';
    +import type { RouterContext } from '../router-module-loader';
    +
    +const app = new Koa();
    +
    +const getUsersV1 = async (ctx: RouterContext) => {
    +  ctx.body = { users: [], version: 'v1' };
    +};
    +
    +const getUserV1 = async (ctx: RouterContext) => {
    +  ctx.body = { user: { id: ctx.params.id }, version: 'v1' };
    +};
    +
    +const getUsersV2 = async (ctx: RouterContext) => {
    +  ctx.body = {
    +    users: [],
    +    version: 'v2',
    +    metadata: { count: 0, timestamp: new Date() }
    +  };
    +};
    +
    +const getUserV2 = async (ctx: RouterContext) => {
    +  ctx.body = {
    +    user: { id: ctx.params.id },
    +    version: 'v2',
    +    links: { self: `/api/v2/users/${ctx.params.id}` }
    +  };
    +};
    +
    +const v1Router = new Router({ prefix: '/api/v1' });
    +v1Router.get('/users', getUsersV1);
    +v1Router.get('/users/:id', getUserV1);
    +
    +const v2Router = new Router({ prefix: '/api/v2' });
    +v2Router.get('/users', getUsersV2);
    +v2Router.get('/users/:id', getUserV2);
    +
    +const apiRouter = new Router({ prefix: '/api' });
    +apiRouter.use(v1Router.routes(), v1Router.allowedMethods());
    +apiRouter.use(v2Router.routes(), v2Router.allowedMethods());
    +
    +app.use(apiRouter.routes());
    +app.use(apiRouter.allowedMethods());
    
  • recipes/authentication-authorization/authentication-authorization.test.ts+99 0 added
    @@ -0,0 +1,99 @@
    +/**
    + * Tests for Authentication & Authorization Recipe
    + */
    +
    +import { describe, it } from 'node:test';
    +import * as assert from 'node:assert';
    +import * as http from 'node:http';
    +import Router, { RouterContext } from '../router-module-loader';
    +import request from 'supertest';
    +import Koa from 'koa';
    +import { Next } from '../common';
    +
    +describe('Authentication & Authorization', () => {
    +  it('should authenticate requests with JWT token', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    const User = {
    +      findById: async (id: string) => ({ id, name: 'John', role: 'user' })
    +    };
    +
    +    const jwt = {
    +      verify: (token: string, _secret: string) => {
    +        if (token === 'valid-token') {
    +          return { userId: '123' };
    +        }
    +        throw new Error('Invalid token');
    +      }
    +    };
    +
    +    const authenticate = async (ctx: RouterContext, next: Next) => {
    +      const authHeader = ctx.headers.authorization || ctx.headers.Authorization;
    +      const token =
    +        typeof authHeader === 'string'
    +          ? authHeader.replace('Bearer ', '')
    +          : undefined;
    +
    +      if (!token) {
    +        ctx.throw(401, 'Authentication required');
    +        return;
    +      }
    +
    +      try {
    +        const decoded = jwt.verify(token, 'secret') as { userId: string };
    +        ctx.state.user = await User.findById(decoded.userId);
    +        return next();
    +      } catch (err) {
    +        ctx.throw(401, 'Invalid token');
    +        return;
    +      }
    +    };
    +
    +    const requireRole =
    +      (role: string) => async (ctx: RouterContext, next: Next) => {
    +        if (!ctx.state.user) {
    +          ctx.throw(401, 'Authentication required');
    +        }
    +
    +        if (ctx.state.user.role !== role) {
    +          ctx.throw(403, 'Insufficient permissions');
    +        }
    +
    +        await next();
    +      };
    +
    +    router.get('/profile', authenticate, async (ctx: RouterContext) => {
    +      ctx.body = ctx.state.user;
    +    });
    +
    +    router.get(
    +      '/admin',
    +      authenticate,
    +      requireRole('admin'),
    +      async (ctx: RouterContext) => {
    +        ctx.body = { message: 'Admin access granted' };
    +      }
    +    );
    +
    +    app.use(router.routes());
    +    app.use(router.allowedMethods());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/profile')
    +      .set('Authorization', 'Bearer valid-token')
    +      .expect(200);
    +
    +    assert.strictEqual(res1.body.id, '123');
    +    assert.strictEqual(res1.body.name, 'John');
    +
    +    await request(http.createServer(app.callback()))
    +      .get('/profile')
    +      .expect(401);
    +
    +    await request(http.createServer(app.callback()))
    +      .get('/admin')
    +      .set('Authorization', 'Bearer valid-token')
    +      .expect(403);
    +  });
    +});
    
  • recipes/authentication-authorization/authentication-authorization.ts+56 0 added
    @@ -0,0 +1,56 @@
    +/**
    + * Authentication & Authorization Recipe
    + *
    + * Implement JWT-based authentication with middleware.
    + *
    + * Note: User model is a placeholder. Replace with your actual user model/service.
    + * Requires: npm install jsonwebtoken @types/jsonwebtoken
    + */
    +import * as jwt from 'jsonwebtoken';
    +import Router from '../router-module-loader';
    +import { User, ContextWithUser, Next } from '../common';
    +
    +const router = new Router();
    +
    +const authenticate = async (ctx: ContextWithUser, next: Next) => {
    +  const token = ctx.headers.authorization?.replace('Bearer ', '');
    +
    +  if (!token) {
    +    ctx.throw(401, 'Authentication required');
    +  }
    +
    +  try {
    +    const decoded = jwt.verify(token, process.env.JWT_SECRET as string);
    +    const user = await User.findById((decoded as any).userId);
    +    ctx.state.user = user || undefined;
    +    await next();
    +  } catch (err) {
    +    ctx.throw(401, 'Invalid token');
    +  }
    +};
    +
    +const requireRole =
    +  (role: string) => async (ctx: ContextWithUser, next: Next) => {
    +    if (!ctx.state.user) {
    +      ctx.throw(401, 'Authentication required');
    +    }
    +
    +    if (ctx.state.user.role !== role) {
    +      ctx.throw(403, 'Insufficient permissions');
    +    }
    +
    +    await next();
    +  };
    +
    +router
    +  .get('/profile', authenticate, async (ctx: ContextWithUser) => {
    +    ctx.body = ctx.state.user;
    +  })
    +  .get(
    +    '/admin',
    +    authenticate,
    +    requireRole('admin'),
    +    async (ctx: ContextWithUser) => {
    +      ctx.body = { message: 'Admin access granted' };
    +    }
    +  );
    
  • recipes/common.ts+136 0 added
    @@ -0,0 +1,136 @@
    +/**
    + * Placeholder types for recipe examples
    + *
    + * These are example types that should be replaced with actual
    + * implementations in real applications.
    + */
    +
    +import type { RouterContext } from './router-module-loader';
    +
    +export interface User {
    +  id: string;
    +  email: string;
    +  name: string;
    +  role?: string;
    +}
    +
    +export interface Post {
    +  id: string;
    +  userId: string;
    +  title: string;
    +  content: string;
    +}
    +
    +export interface Resource {
    +  id: string;
    +  name: string;
    +}
    +
    +export type ContextWithBody<StateT = any, ContextT = any> = RouterContext<
    +  StateT,
    +  ContextT
    +> & {
    +  request: RouterContext<StateT, ContextT>['request'] & {
    +    body?: any;
    +  };
    +};
    +
    +export type ContextWithUser<StateT = any, ContextT = any> = RouterContext<
    +  StateT,
    +  ContextT
    +> & {
    +  state: RouterContext<StateT, ContextT>['state'] & {
    +    user?: User;
    +    resource?: Resource;
    +    pagination?: {
    +      page: number;
    +      limit: number;
    +      offset: number;
    +    };
    +  };
    +};
    +
    +export type RecipeContext<StateT = any, ContextT = any> = RouterContext<
    +  StateT,
    +  ContextT
    +> & {
    +  request: RouterContext<StateT, ContextT>['request'] & {
    +    body?: any;
    +  };
    +  state: RouterContext<StateT, ContextT>['state'] & {
    +    user?: User;
    +    resource?: Resource;
    +    pagination?: {
    +      page: number;
    +      limit: number;
    +      offset: number;
    +    };
    +  };
    +};
    +
    +export type { Next } from 'koa';
    +
    +export const db = {
    +  authenticate: async (): Promise<void> => {}
    +};
    +
    +export const redis = {
    +  ping: async (): Promise<string> => {
    +    return 'PONG';
    +  }
    +};
    +
    +export const User = {
    +  findById: async (_id: string): Promise<User | null> => {
    +    return null;
    +  },
    +  findAll: async (): Promise<User[]> => {
    +    return [];
    +  },
    +  findAndCountAll: async (_options: {
    +    limit: number;
    +    offset: number;
    +  }): Promise<{ count: number; rows: User[] }> => {
    +    return { count: 0, rows: [] };
    +  },
    +  create: async (data: any): Promise<User> => {
    +    return { id: '1', email: data.email || '', name: data.name || '' };
    +  },
    +  update: async (_id: string, data: any): Promise<User> => {
    +    return { id: _id, email: data.email || '', name: data.name || '' };
    +  },
    +  delete: async (_id: string): Promise<void> => {}
    +};
    +
    +export const Post = {
    +  findAll: async (_options: {
    +    limit: number;
    +    offset: number;
    +  }): Promise<Post[]> => {
    +    return [];
    +  },
    +  findByUserId: async (_userId: string): Promise<Post[]> => {
    +    return [];
    +  }
    +};
    +
    +export const Resource = {
    +  findById: async (_id: string): Promise<Resource | null> => {
    +    return null;
    +  }
    +};
    +
    +export const createUser = async (data: {
    +  email: string;
    +  password: string;
    +  name: string;
    +}): Promise<User> => {
    +  return { id: '1', email: data.email, name: data.name };
    +};
    +
    +export const updateUser = async (
    +  _id: string,
    +  data: { email?: string; name?: string }
    +): Promise<User> => {
    +  return { id: _id, email: data.email || '', name: data.name || '' };
    +};
    
  • recipes/error-handling/error-handling.test.ts+126 0 added
    @@ -0,0 +1,126 @@
    +/**
    + * Tests for Error Handling Recipe
    + */
    +
    +import { describe, it } from 'node:test';
    +import * as assert from 'node:assert';
    +import * as http from 'node:http';
    +import Router, { RouterContext } from '../router-module-loader';
    +import request from 'supertest';
    +import Koa from 'koa';
    +import { Next } from '../common';
    +
    +describe('Error Handling', () => {
    +  it('should handle errors with custom error class', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    class AppError extends Error {
    +      status: number;
    +      code: string;
    +      isOperational: boolean;
    +      details?: any;
    +
    +      constructor(
    +        message: string,
    +        status = 500,
    +        code = 'INTERNAL_ERROR',
    +        details?: any
    +      ) {
    +        super(message);
    +        this.status = status;
    +        this.code = code;
    +        this.isOperational = true;
    +        this.details = details;
    +      }
    +    }
    +
    +    const errorHandler = async (ctx: RouterContext, next: Next) => {
    +      try {
    +        await next();
    +      } catch (err: any) {
    +        ctx.status = err.status || 500;
    +        ctx.body = {
    +          error: {
    +            message: err.message,
    +            code: err.code || 'INTERNAL_ERROR',
    +            ...(process.env.NODE_ENV === 'development' && {
    +              stack: err.stack,
    +              details: err.details
    +            })
    +          }
    +        };
    +
    +        ctx.app.emit('error', err, ctx);
    +      }
    +    };
    +
    +    const User = {
    +      findById: async (id: string) => {
    +        if (id === '123') {
    +          return { id: '123', name: 'John' };
    +        }
    +        return null;
    +      }
    +    };
    +
    +    router.get('/users/:id', async (ctx: RouterContext) => {
    +      const user = await User.findById(ctx.params.id);
    +      if (!user) {
    +        throw new AppError('User not found', 404, 'USER_NOT_FOUND');
    +      }
    +      ctx.body = user;
    +    });
    +
    +    app.use(errorHandler);
    +    app.use(router.routes());
    +    app.use(router.allowedMethods({ throw: true }));
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/users/123')
    +      .expect(200);
    +
    +    assert.strictEqual(res1.body.id, '123');
    +    assert.strictEqual(res1.body.name, 'John');
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/users/999')
    +      .expect(404);
    +
    +    assert.strictEqual(res2.body.error.message, 'User not found');
    +    assert.strictEqual(res2.body.error.code, 'USER_NOT_FOUND');
    +  });
    +
    +  it('should handle generic errors', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    const errorHandler = async (ctx: RouterContext, next: Next) => {
    +      try {
    +        await next();
    +      } catch (err: any) {
    +        ctx.status = err.status || 500;
    +        ctx.body = {
    +          error: {
    +            message: err.message,
    +            code: err.code || 'INTERNAL_ERROR'
    +          }
    +        };
    +      }
    +    };
    +
    +    router.get('/error', async () => {
    +      throw new Error('Something went wrong');
    +    });
    +
    +    app.use(errorHandler);
    +    app.use(router.routes());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .get('/error')
    +      .expect(500);
    +
    +    assert.strictEqual(res.body.error.message, 'Something went wrong');
    +    assert.strictEqual(res.body.error.code, 'INTERNAL_ERROR');
    +  });
    +});
    
  • recipes/error-handling/error-handling.ts+66 0 added
    @@ -0,0 +1,66 @@
    +/**
    + * Error Handling Recipe
    + *
    + * Centralized error handling with custom error classes.
    + *
    + * Note: User model is a placeholder. Replace with your actual model/service.
    + */
    +import Koa from 'koa';
    +import Router from '../router-module-loader';
    +import { User, Next } from '../common';
    +import type { RouterContext } from '../router-module-loader';
    +
    +const app = new Koa();
    +const router = new Router();
    +
    +class AppError extends Error {
    +  status: number;
    +  code: string;
    +  isOperational: boolean;
    +  details?: any;
    +
    +  constructor(
    +    message: string,
    +    status = 500,
    +    code = 'INTERNAL_ERROR',
    +    details?: any
    +  ) {
    +    super(message);
    +    this.status = status;
    +    this.code = code;
    +    this.isOperational = true;
    +    this.details = details;
    +  }
    +}
    +
    +const errorHandler = async (ctx: RouterContext, next: Next) => {
    +  try {
    +    await next();
    +  } catch (err: any) {
    +    ctx.status = err.status || 500;
    +    ctx.body = {
    +      error: {
    +        message: err.message,
    +        code: err.code || 'INTERNAL_ERROR',
    +        ...(process.env.NODE_ENV === 'development' && {
    +          stack: err.stack,
    +          details: err.details
    +        })
    +      }
    +    };
    +
    +    ctx.app.emit('error', err, ctx);
    +  }
    +};
    +
    +router.get('/users/:id', async (ctx: RouterContext) => {
    +  const user = await User.findById(ctx.params.id);
    +  if (!user) {
    +    throw new AppError('User not found', 404, 'USER_NOT_FOUND');
    +  }
    +  ctx.body = user;
    +});
    +
    +app.use(errorHandler);
    +app.use(router.routes());
    +app.use(router.allowedMethods({ throw: true }));
    
  • recipes/health-checks/health-checks.test.ts+114 0 added
    @@ -0,0 +1,114 @@
    +/**
    + * Tests for Health Checks Recipe
    + */
    +
    +import { describe, it } from 'node:test';
    +import * as assert from 'node:assert';
    +import * as http from 'node:http';
    +import Router, { RouterContext } from '../router-module-loader';
    +import request from 'supertest';
    +import Koa from 'koa';
    +
    +describe('Health Checks', () => {
    +  it('should provide health check endpoint', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    const db = {
    +      authenticate: async () => Promise.resolve()
    +    };
    +
    +    const redis = {
    +      ping: async () => Promise.resolve('PONG')
    +    };
    +
    +    router.get('/health', async (ctx: RouterContext) => {
    +      const health = {
    +        status: 'ok',
    +        timestamp: new Date().toISOString(),
    +        uptime: process.uptime(),
    +        checks: {} as Record<string, string>
    +      };
    +
    +      try {
    +        await db.authenticate();
    +        health.checks.database = 'ok';
    +      } catch (err) {
    +        health.checks.database = 'error';
    +        health.status = 'degraded';
    +      }
    +
    +      try {
    +        await redis.ping();
    +        health.checks.redis = 'ok';
    +      } catch (err) {
    +        health.checks.redis = 'error';
    +        health.status = 'degraded';
    +      }
    +
    +      ctx.status = health.status === 'ok' ? 200 : 503;
    +      ctx.body = health;
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .get('/health')
    +      .expect(200);
    +
    +    assert.strictEqual(res.body.status, 'ok');
    +    assert.strictEqual(res.body.checks.database, 'ok');
    +    assert.strictEqual(res.body.checks.redis, 'ok');
    +    assert.strictEqual(typeof res.body.uptime, 'number');
    +    assert.strictEqual(typeof res.body.timestamp, 'string');
    +  });
    +
    +  it('should provide readiness probe', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    let isReady = true;
    +
    +    const checkReadiness = async (): Promise<boolean> => {
    +      return isReady;
    +    };
    +
    +    router.get('/ready', async (ctx: RouterContext) => {
    +      const ready = await checkReadiness();
    +      ctx.status = ready ? 200 : 503;
    +      ctx.body = { ready };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/ready')
    +      .expect(200);
    +
    +    assert.strictEqual(res1.body.ready, true);
    +
    +    isReady = false;
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/ready')
    +      .expect(503);
    +
    +    assert.strictEqual(res2.body.ready, false);
    +  });
    +
    +  it('should provide liveness probe', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.get('/live', async (ctx: RouterContext) => {
    +      ctx.body = { alive: true };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .get('/live')
    +      .expect(200);
    +
    +    assert.strictEqual(res.body.alive, true);
    +  });
    +});
    
  • recipes/health-checks/health-checks.ts+64 0 added
    @@ -0,0 +1,64 @@
    +/**
    + * Health Checks Recipe
    + *
    + * Add health check endpoints for monitoring and orchestration.
    + *
    + * Note: db and redis are placeholders. Replace with your actual
    + * database and cache clients.
    + */
    +import Router from '../router-module-loader';
    +import { db, redis } from '../common';
    +import type { RouterContext } from '../router-module-loader';
    +
    +const router = new Router();
    +
    +router.get('/health', async (ctx: RouterContext) => {
    +  const health = {
    +    status: 'ok',
    +    timestamp: new Date().toISOString(),
    +    uptime: process.uptime(),
    +    checks: {} as Record<string, string>
    +  };
    +
    +  try {
    +    await db.authenticate();
    +    health.checks.database = 'ok';
    +  } catch (err) {
    +    health.checks.database = 'error';
    +    health.status = 'degraded';
    +  }
    +
    +  try {
    +    await redis.ping();
    +    health.checks.redis = 'ok';
    +  } catch (err) {
    +    health.checks.redis = 'error';
    +    health.status = 'degraded';
    +  }
    +
    +  ctx.status = health.status === 'ok' ? 200 : 503;
    +  ctx.body = health;
    +});
    +
    +router.get('/ready', async (ctx: RouterContext) => {
    +  const isReady = await checkReadiness();
    +  ctx.status = isReady ? 200 : 503;
    +  ctx.body = { ready: isReady };
    +});
    +
    +router.get('/live', async (ctx: RouterContext) => {
    +  ctx.body = { alive: true };
    +});
    +
    +async function checkReadiness(): Promise<boolean> {
    +  try {
    +    await db.authenticate();
    +
    +    // Check other critical services
    +    // await redis.ping();
    +
    +    return true;
    +  } catch (err) {
    +    return false;
    +  }
    +}
    
  • recipes/nested-routes/nested-routes.test.ts+314 0 added
    @@ -0,0 +1,314 @@
    +/**
    + * Tests for Production-Ready Nested Routes Recipe
    + *
    + * Tests multiple levels of nesting, parameter propagation, and real-world scenarios.
    + */
    +
    +import { describe, it } from 'node:test';
    +import * as assert from 'node:assert';
    +import * as http from 'node:http';
    +import Koa from 'koa';
    +import Router from '../router-module-loader';
    +import request from 'supertest';
    +
    +describe('Production-Ready Nested Routes', () => {
    +  it('should handle multiple levels of nested routes correctly', async () => {
    +    const app = new Koa();
    +
    +    app.use(async (ctx, next) => {
    +      if (ctx.request.is('application/json')) {
    +        let body = '';
    +        for await (const chunk of ctx.req) {
    +          body += chunk;
    +        }
    +        try {
    +          (ctx.request as any).body = JSON.parse(body);
    +        } catch {
    +          (ctx.request as any).body = {};
    +        }
    +      }
    +      await next();
    +    });
    +
    +    // ============================================================================
    +    // Level 1: API Version Router
    +    // ============================================================================
    +    const apiV1Router = new Router({ prefix: '/api/v1' });
    +
    +    // ============================================================================
    +    // Level 2: Users Router
    +    // ============================================================================
    +    const usersRouter = new Router({ prefix: '/users' });
    +
    +    usersRouter.get('/', async (ctx: any) => {
    +      ctx.body = { users: [{ id: '1', name: 'John' }] };
    +    });
    +
    +    usersRouter.get('/:userId', async (ctx: any) => {
    +      ctx.body = { id: ctx.params.userId, name: 'John' };
    +    });
    +
    +    // ============================================================================
    +    // Level 3: User Posts Router
    +    // ============================================================================
    +    const userPostsRouter = new Router({ prefix: '/:userId/posts' });
    +
    +    userPostsRouter.get('/', async (ctx: any) => {
    +      ctx.body = {
    +        userId: ctx.params.userId,
    +        posts: [{ id: '1', title: 'Post 1' }]
    +      };
    +    });
    +
    +    userPostsRouter.get('/:postId', async (ctx: any) => {
    +      ctx.body = {
    +        id: ctx.params.postId,
    +        userId: ctx.params.userId,
    +        title: 'Post Title'
    +      };
    +    });
    +
    +    userPostsRouter.post('/', async (ctx: any) => {
    +      ctx.body = {
    +        id: '2',
    +        userId: ctx.params.userId,
    +        ...(ctx.request as any).body
    +      };
    +    });
    +
    +    // ============================================================================
    +    // Level 4: Post Comments Router
    +    // ============================================================================
    +    const postCommentsRouter = new Router({ prefix: '/:postId/comments' });
    +
    +    postCommentsRouter.get('/', async (ctx: any) => {
    +      ctx.body = {
    +        postId: ctx.params.postId,
    +        userId: ctx.params.userId,
    +        comments: [{ id: '1', text: 'Comment 1' }]
    +      };
    +    });
    +
    +    postCommentsRouter.get('/:commentId', async (ctx: any) => {
    +      ctx.body = {
    +        id: ctx.params.commentId,
    +        postId: ctx.params.postId,
    +        userId: ctx.params.userId,
    +        text: 'Comment text'
    +      };
    +    });
    +
    +    postCommentsRouter.post('/', async (ctx: any) => {
    +      ctx.body = {
    +        id: '2',
    +        postId: ctx.params.postId,
    +        userId: ctx.params.userId,
    +        ...(ctx.request as any).body
    +      };
    +    });
    +
    +    // ============================================================================
    +    // Level 3: User Settings Router
    +    // ============================================================================
    +    const userSettingsRouter = new Router({ prefix: '/:userId/settings' });
    +
    +    userSettingsRouter.get('/', async (ctx: any) => {
    +      ctx.body = {
    +        userId: ctx.params.userId,
    +        theme: 'dark'
    +      };
    +    });
    +
    +    userSettingsRouter.put('/', async (ctx: any) => {
    +      ctx.body = {
    +        userId: ctx.params.userId,
    +        ...(ctx.request as any).body
    +      };
    +    });
    +
    +    // ============================================================================
    +    // Mounting: Assemble nested structure
    +    // ============================================================================
    +    userPostsRouter.use(
    +      postCommentsRouter.routes(),
    +      postCommentsRouter.allowedMethods()
    +    );
    +    usersRouter.use(userPostsRouter.routes(), userPostsRouter.allowedMethods());
    +    usersRouter.use(
    +      userSettingsRouter.routes(),
    +      userSettingsRouter.allowedMethods()
    +    );
    +    apiV1Router.use(usersRouter.routes(), usersRouter.allowedMethods());
    +    app.use(apiV1Router.routes());
    +    app.use(apiV1Router.allowedMethods());
    +
    +    const server = http.createServer(app.callback());
    +
    +    // Test Level 2: Users routes
    +    const res1 = await request(server).get('/api/v1/users').expect(200);
    +    assert.strictEqual(Array.isArray(res1.body.users), true);
    +
    +    const res2 = await request(server).get('/api/v1/users/123').expect(200);
    +    assert.strictEqual(res2.body.id, '123');
    +
    +    // Test Level 3: User Posts routes
    +    const res3 = await request(server)
    +      .get('/api/v1/users/123/posts')
    +      .expect(200);
    +    assert.strictEqual(res3.body.userId, '123');
    +    assert.strictEqual(Array.isArray(res3.body.posts), true);
    +
    +    const res4 = await request(server)
    +      .get('/api/v1/users/123/posts/456')
    +      .expect(200);
    +    assert.strictEqual(res4.body.id, '456');
    +    assert.strictEqual(res4.body.userId, '123');
    +
    +    const res5 = await request(server)
    +      .post('/api/v1/users/123/posts')
    +      .send({ title: 'New Post' })
    +      .expect(200);
    +    assert.strictEqual(res5.body.userId, '123');
    +    assert.strictEqual(res5.body.title, 'New Post');
    +
    +    // Test Level 4: Post Comments routes (deeply nested)
    +    const res6 = await request(server)
    +      .get('/api/v1/users/123/posts/456/comments')
    +      .expect(200);
    +    assert.strictEqual(res6.body.userId, '123');
    +    assert.strictEqual(res6.body.postId, '456');
    +    assert.strictEqual(Array.isArray(res6.body.comments), true);
    +
    +    const res7 = await request(server)
    +      .get('/api/v1/users/123/posts/456/comments/789')
    +      .expect(200);
    +    assert.strictEqual(res7.body.userId, '123');
    +    assert.strictEqual(res7.body.postId, '456');
    +    assert.strictEqual(res7.body.id, '789');
    +
    +    const res8 = await request(server)
    +      .post('/api/v1/users/123/posts/456/comments')
    +      .send({ text: 'New Comment' })
    +      .expect(200);
    +    assert.strictEqual(res8.body.userId, '123');
    +    assert.strictEqual(res8.body.postId, '456');
    +    assert.strictEqual(res8.body.text, 'New Comment');
    +
    +    // Test Level 3: User Settings routes
    +    const res9 = await request(server)
    +      .get('/api/v1/users/123/settings')
    +      .expect(200);
    +    assert.strictEqual(res9.body.userId, '123');
    +
    +    const res10 = await request(server)
    +      .put('/api/v1/users/123/settings')
    +      .send({ theme: 'light' })
    +      .expect(200);
    +    assert.strictEqual(res10.body.userId, '123');
    +    assert.strictEqual(res10.body.theme, 'light');
    +  });
    +
    +  it('should propagate parameters correctly through nested routers', async () => {
    +    const app = new Koa();
    +    const apiRouter = new Router({ prefix: '/api' });
    +    const usersRouter = new Router({ prefix: '/users' });
    +    const postsRouter = new Router({ prefix: '/:userId/posts' });
    +    const commentsRouter = new Router({ prefix: '/:postId/comments' });
    +
    +    commentsRouter.get('/:commentId', async (ctx: any) => {
    +      ctx.body = {
    +        userId: ctx.params.userId,
    +        postId: ctx.params.postId,
    +        commentId: ctx.params.commentId,
    +        allParams: ctx.params
    +      };
    +    });
    +
    +    postsRouter.use(commentsRouter.routes(), commentsRouter.allowedMethods());
    +    usersRouter.use(postsRouter.routes(), postsRouter.allowedMethods());
    +    apiRouter.use(usersRouter.routes(), usersRouter.allowedMethods());
    +    app.use(apiRouter.routes());
    +    app.use(apiRouter.allowedMethods());
    +
    +    const server = http.createServer(app.callback());
    +
    +    const res = await request(server)
    +      .get('/api/users/user123/posts/post456/comments/comment789')
    +      .expect(200);
    +
    +    assert.strictEqual(res.body.userId, 'user123');
    +    assert.strictEqual(res.body.postId, 'post456');
    +    assert.strictEqual(res.body.commentId, 'comment789');
    +    assert.deepStrictEqual(res.body.allParams, {
    +      userId: 'user123',
    +      postId: 'post456',
    +      commentId: 'comment789'
    +    });
    +  });
    +
    +  it('should handle multiple nested resources at the same level', async () => {
    +    const app = new Koa();
    +    const apiRouter = new Router({ prefix: '/api' });
    +    const usersRouter = new Router({ prefix: '/users' });
    +
    +    const postsRouter = new Router({ prefix: '/:userId/posts' });
    +    const settingsRouter = new Router({ prefix: '/:userId/settings' });
    +    const followersRouter = new Router({ prefix: '/:userId/followers' });
    +
    +    postsRouter.get('/', async (ctx: any) => {
    +      ctx.body = { userId: ctx.params.userId, resource: 'posts' };
    +    });
    +
    +    settingsRouter.get('/', async (ctx: any) => {
    +      ctx.body = { userId: ctx.params.userId, resource: 'settings' };
    +    });
    +
    +    followersRouter.get('/', async (ctx: any) => {
    +      ctx.body = { userId: ctx.params.userId, resource: 'followers' };
    +    });
    +
    +    usersRouter.use(postsRouter.routes(), postsRouter.allowedMethods());
    +    usersRouter.use(settingsRouter.routes(), settingsRouter.allowedMethods());
    +    usersRouter.use(followersRouter.routes(), followersRouter.allowedMethods());
    +    apiRouter.use(usersRouter.routes(), usersRouter.allowedMethods());
    +    app.use(apiRouter.routes());
    +    app.use(apiRouter.allowedMethods());
    +
    +    const server = http.createServer(app.callback());
    +
    +    const res1 = await request(server).get('/api/users/123/posts').expect(200);
    +    assert.strictEqual(res1.body.resource, 'posts');
    +
    +    const res2 = await request(server)
    +      .get('/api/users/123/settings')
    +      .expect(200);
    +    assert.strictEqual(res2.body.resource, 'settings');
    +
    +    const res3 = await request(server)
    +      .get('/api/users/123/followers')
    +      .expect(200);
    +    assert.strictEqual(res3.body.resource, 'followers');
    +  });
    +
    +  it('should handle 405 Method Not Allowed correctly for nested routes', async () => {
    +    const app = new Koa();
    +    const apiRouter = new Router({ prefix: '/api' });
    +    const usersRouter = new Router({ prefix: '/users' });
    +    const postsRouter = new Router({ prefix: '/:userId/posts' });
    +
    +    postsRouter.get('/:postId', async (ctx: any) => {
    +      ctx.body = { id: ctx.params.postId };
    +    });
    +
    +    usersRouter.use(postsRouter.routes(), postsRouter.allowedMethods());
    +    apiRouter.use(usersRouter.routes(), usersRouter.allowedMethods());
    +    app.use(apiRouter.routes());
    +    app.use(apiRouter.allowedMethods());
    +
    +    const server = http.createServer(app.callback());
    +
    +    await request(server).get('/api/users/123/posts/456').expect(200);
    +
    +    await request(server).post('/api/users/123/posts/456').expect(405);
    +  });
    +});
    
  • recipes/nested-routes/nested-routes.ts+224 0 added
    @@ -0,0 +1,224 @@
    +/**
    + * Production-Ready Nested Routes Recipe
    + *
    + * Demonstrates advanced nested router patterns used in production applications:
    + * - Multiple levels of nesting (3+ levels deep)
    + * - Parameter propagation through nested routers
    + * - Middleware at different nesting levels
    + * - Multiple resources organized hierarchically
    + * - Proper error handling and validation
    + *
    + * This pattern is commonly used in:
    + * - RESTful APIs with versioning
    + * - Multi-tenant applications
    + * - Resource hierarchies (e.g., /users/:userId/posts/:postId/comments)
    + * - Admin panels with nested sections
    + */
    +
    +import Koa from 'koa';
    +import Router from '../router-module-loader';
    +import type { RouterContext } from '../router-module-loader';
    +import type { ContextWithBody, Next } from '../common';
    +
    +const app = new Koa();
    +
    +const apiV1Router = new Router({ prefix: '/api/v1' });
    +
    +apiV1Router.use(async (ctx: RouterContext, next: Next) => {
    +  console.log(`[API v1] ${ctx.method} ${ctx.path}`);
    +  ctx.state.apiVersion = 'v1';
    +  await next();
    +});
    +
    +const usersRouter = new Router({ prefix: '/users' });
    +
    +usersRouter.use(async (_ctx: RouterContext, next: Next) => {
    +  console.log('[Users Router] Processing user request');
    +  await next();
    +});
    +
    +usersRouter.get('/', async (ctx: RouterContext) => {
    +  ctx.body = {
    +    users: [
    +      { id: '1', name: 'John', email: 'john@example.com' },
    +      { id: '2', name: 'Jane', email: 'jane@example.com' }
    +    ]
    +  };
    +});
    +
    +usersRouter.post('/', async (ctx: ContextWithBody) => {
    +  ctx.body = {
    +    id: '3',
    +    ...(ctx.request.body || {}),
    +    createdAt: new Date().toISOString()
    +  };
    +});
    +
    +usersRouter.get('/:userId', async (ctx: RouterContext) => {
    +  ctx.body = {
    +    id: ctx.params.userId,
    +    name: 'John',
    +    email: 'john@example.com'
    +  };
    +});
    +
    +usersRouter.put('/:userId', async (ctx: ContextWithBody) => {
    +  ctx.body = {
    +    id: ctx.params.userId,
    +    ...(ctx.request.body || {}),
    +    updatedAt: new Date().toISOString()
    +  };
    +});
    +
    +usersRouter.delete('/:userId', async (ctx: RouterContext) => {
    +  ctx.status = 204;
    +});
    +
    +const userPostsRouter = new Router({ prefix: '/:userId/posts' });
    +
    +userPostsRouter.use(async (ctx: RouterContext, next: Next) => {
    +  console.log(`[User Posts] Loading posts for user ${ctx.params.userId}`);
    +  ctx.state.userId = ctx.params.userId;
    +  await next();
    +});
    +
    +userPostsRouter.get('/', async (ctx: RouterContext) => {
    +  ctx.body = {
    +    userId: ctx.params.userId,
    +    posts: [
    +      { id: '1', title: 'Post 1', userId: ctx.params.userId },
    +      { id: '2', title: 'Post 2', userId: ctx.params.userId }
    +    ]
    +  };
    +});
    +
    +userPostsRouter.post('/', async (ctx: ContextWithBody) => {
    +  ctx.body = {
    +    id: '3',
    +    userId: ctx.params.userId,
    +    ...(ctx.request.body || {}),
    +    createdAt: new Date().toISOString()
    +  };
    +});
    +
    +userPostsRouter.get('/:postId', async (ctx: RouterContext) => {
    +  ctx.body = {
    +    id: ctx.params.postId,
    +    userId: ctx.params.userId,
    +    title: 'Post Title',
    +    content: 'Post content...'
    +  };
    +});
    +
    +userPostsRouter.put('/:postId', async (ctx: ContextWithBody) => {
    +  ctx.body = {
    +    id: ctx.params.postId,
    +    userId: ctx.params.userId,
    +    ...(ctx.request.body || {}),
    +    updatedAt: new Date().toISOString()
    +  };
    +});
    +
    +userPostsRouter.delete('/:postId', async (ctx: RouterContext) => {
    +  ctx.status = 204;
    +});
    +
    +const postCommentsRouter = new Router({ prefix: '/:postId/comments' });
    +
    +postCommentsRouter.use(async (ctx: RouterContext, next: Next) => {
    +  console.log(
    +    `[Comments] Loading comments for post ${ctx.params.postId} by user ${ctx.params.userId}`
    +  );
    +  ctx.state.postId = ctx.params.postId;
    +  await next();
    +});
    +
    +postCommentsRouter.get('/', async (ctx: RouterContext) => {
    +  ctx.body = {
    +    postId: ctx.params.postId,
    +    userId: ctx.params.userId,
    +    comments: [
    +      { id: '1', text: 'Comment 1', postId: ctx.params.postId },
    +      { id: '2', text: 'Comment 2', postId: ctx.params.postId }
    +    ]
    +  };
    +});
    +
    +postCommentsRouter.post('/', async (ctx: ContextWithBody) => {
    +  ctx.body = {
    +    id: '3',
    +    postId: ctx.params.postId,
    +    userId: ctx.params.userId,
    +    ...(ctx.request.body || {}),
    +    createdAt: new Date().toISOString()
    +  };
    +});
    +
    +postCommentsRouter.get('/:commentId', async (ctx: RouterContext) => {
    +  ctx.body = {
    +    id: ctx.params.commentId,
    +    postId: ctx.params.postId,
    +    userId: ctx.params.userId,
    +    text: 'Comment text...'
    +  };
    +});
    +
    +postCommentsRouter.delete('/:commentId', async (ctx: RouterContext) => {
    +  ctx.status = 204;
    +});
    +
    +const userSettingsRouter = new Router({ prefix: '/:userId/settings' });
    +
    +userSettingsRouter.get('/', async (ctx: RouterContext) => {
    +  ctx.body = {
    +    userId: ctx.params.userId,
    +    theme: 'dark',
    +    notifications: true
    +  };
    +});
    +
    +userSettingsRouter.put('/', async (ctx: ContextWithBody) => {
    +  ctx.body = {
    +    userId: ctx.params.userId,
    +    ...(ctx.request.body || {}),
    +    updatedAt: new Date().toISOString()
    +  };
    +});
    +
    +userPostsRouter.use(
    +  postCommentsRouter.routes(),
    +  postCommentsRouter.allowedMethods()
    +);
    +
    +usersRouter.use(userPostsRouter.routes(), userPostsRouter.allowedMethods());
    +usersRouter.use(
    +  userSettingsRouter.routes(),
    +  userSettingsRouter.allowedMethods()
    +);
    +
    +apiV1Router.use(usersRouter.routes(), usersRouter.allowedMethods());
    +
    +const postsRouter = new Router({ prefix: '/posts' });
    +
    +postsRouter.get('/', async (ctx: RouterContext) => {
    +  ctx.body = {
    +    posts: [
    +      { id: '1', title: 'Global Post 1' },
    +      { id: '2', title: 'Global Post 2' }
    +    ]
    +  };
    +});
    +
    +postsRouter.get('/:postId', async (ctx: RouterContext) => {
    +  ctx.body = {
    +    id: ctx.params.postId,
    +    title: 'Global Post Title'
    +  };
    +});
    +
    +apiV1Router.use(postsRouter.routes(), postsRouter.allowedMethods());
    +
    +app.use(apiV1Router.routes());
    +app.use(apiV1Router.allowedMethods());
    +
    +export default app;
    
  • recipes/pagination/pagination.test.ts+99 0 added
    @@ -0,0 +1,99 @@
    +/**
    + * Tests for Pagination Recipe
    + */
    +
    +import { describe, it } from 'node:test';
    +import * as assert from 'node:assert';
    +import * as http from 'node:http';
    +import Router, { RouterContext } from '../router-module-loader';
    +import request from 'supertest';
    +import Koa from 'koa';
    +import { Next } from '../common';
    +
    +describe('Pagination', () => {
    +  it('should paginate list endpoints', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    interface PaginationState {
    +      page: number;
    +      limit: number;
    +      offset: number;
    +    }
    +
    +    const User = {
    +      findAndCountAll: async (options: { limit: number; offset: number }) => {
    +        const allUsers = [
    +          { id: 1, name: 'User 1' },
    +          { id: 2, name: 'User 2' },
    +          { id: 3, name: 'User 3' },
    +          { id: 4, name: 'User 4' },
    +          { id: 5, name: 'User 5' }
    +        ];
    +
    +        const start = options.offset;
    +        const end = start + options.limit;
    +        const rows = allUsers.slice(start, end);
    +
    +        return {
    +          count: allUsers.length,
    +          rows
    +        };
    +      }
    +    };
    +
    +    const paginate = async (ctx: RouterContext, next: Next) => {
    +      const page = parseInt(ctx.query.page as string) || 1;
    +      const limit = parseInt(ctx.query.limit as string) || 10;
    +      const offset = (page - 1) * limit;
    +
    +      ctx.state.pagination = { page, limit, offset } as PaginationState;
    +      await next();
    +    };
    +
    +    router.get('/users', paginate, async (ctx: RouterContext) => {
    +      const { limit, offset } = ctx.state.pagination as PaginationState;
    +      const { count, rows } = await User.findAndCountAll({ limit, offset });
    +
    +      ctx.set('X-Total-Count', count.toString());
    +      ctx.set('X-Page-Count', Math.ceil(count / limit).toString());
    +      ctx.body = {
    +        data: rows,
    +        pagination: {
    +          page: ctx.state.pagination.page,
    +          limit,
    +          total: count,
    +          pages: Math.ceil(count / limit)
    +        }
    +      };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/users?page=1&limit=2')
    +      .expect(200);
    +
    +    assert.strictEqual(res1.body.data.length, 2);
    +    assert.strictEqual(res1.body.pagination.page, 1);
    +    assert.strictEqual(res1.body.pagination.limit, 2);
    +    assert.strictEqual(res1.body.pagination.total, 5);
    +    assert.strictEqual(res1.body.pagination.pages, 3);
    +    assert.strictEqual(res1.headers['x-total-count'], '5');
    +    assert.strictEqual(res1.headers['x-page-count'], '3');
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/users?page=2&limit=2')
    +      .expect(200);
    +
    +    assert.strictEqual(res2.body.data.length, 2);
    +    assert.strictEqual(res2.body.pagination.page, 2);
    +
    +    const res3 = await request(http.createServer(app.callback()))
    +      .get('/users')
    +      .expect(200);
    +
    +    assert.strictEqual(res3.body.pagination.page, 1);
    +    assert.strictEqual(res3.body.pagination.limit, 10);
    +  });
    +});
    
  • recipes/pagination/pagination.ts+60 0 added
    @@ -0,0 +1,60 @@
    +/**
    + * Pagination Recipe
    + *
    + * Implement pagination for list endpoints.
    + *
    + * Note: User and Post models are placeholders.
    + * Replace with your actual models/services.
    + */
    +import Router from '../router-module-loader';
    +import { User, Post, ContextWithUser, Next } from '../common';
    +
    +const router = new Router();
    +
    +const paginate = async (ctx: ContextWithUser, next: Next) => {
    +  const page = parseInt(ctx.query.page as string) || 1;
    +  const limit = parseInt(ctx.query.limit as string) || 10;
    +  const offset = (page - 1) * limit;
    +
    +  ctx.state.pagination = { page, limit, offset };
    +  await next();
    +};
    +
    +router.get('/users', paginate, async (ctx: ContextWithUser) => {
    +  const { limit, offset } = ctx.state.pagination || {
    +    page: 1,
    +    limit: 10,
    +    offset: 0
    +  };
    +  const { count, rows } = await User.findAndCountAll({
    +    limit,
    +    offset
    +  });
    +
    +  ctx.set('X-Total-Count', count.toString());
    +  ctx.set('X-Page-Count', Math.ceil(count / limit).toString());
    +  const pagination = ctx.state.pagination || { page: 1, limit: 10, offset: 0 };
    +  ctx.body = {
    +    data: rows,
    +    pagination: {
    +      page: pagination.page,
    +      limit,
    +      total: count,
    +      pages: Math.ceil(count / limit)
    +    }
    +  };
    +});
    +
    +const getPaginationParams = (ctx: ContextWithUser) => {
    +  const page = parseInt(ctx.query.page as string) || 1;
    +  const limit = parseInt(ctx.query.limit as string) || 10;
    +  const offset = (page - 1) * limit;
    +
    +  return { page, limit, offset };
    +};
    +
    +router.get('/posts', async (ctx: ContextWithUser) => {
    +  const { limit, offset } = getPaginationParams(ctx);
    +  const posts = await Post.findAll({ limit, offset });
    +  ctx.body = posts;
    +});
    
  • recipes/parameter-validation/parameter-validation.test.ts+139 0 added
    @@ -0,0 +1,139 @@
    +/**
    + * Tests for Parameter Validation Recipe
    + */
    +
    +import { describe, it } from 'node:test';
    +import * as assert from 'node:assert';
    +import * as http from 'node:http';
    +import Router, { RouterContext } from '../router-module-loader';
    +import request from 'supertest';
    +import Koa from 'koa';
    +import { Next } from '../common';
    +
    +describe('Parameter Validation', () => {
    +  it('should validate UUID format with router.param()', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.param('id', (value: string, ctx: RouterContext, next: Next) => {
    +      const uuidRegex =
    +        /^[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}$/;
    +
    +      if (!uuidRegex.test(value)) {
    +        ctx.throw(400, 'Invalid ID format');
    +      }
    +
    +      return next();
    +    });
    +
    +    router.get('/users/:id', (ctx: RouterContext) => {
    +      ctx.body = { id: ctx.params.id, valid: true };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const validUUID = '123e4567-e89b-12d3-a456-426614174000';
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get(`/users/${validUUID}`)
    +      .expect(200);
    +
    +    assert.strictEqual(res1.body.id, validUUID);
    +    assert.strictEqual(res1.body.valid, true);
    +
    +    await request(http.createServer(app.callback()))
    +      .get('/users/invalid-id')
    +      .expect(400);
    +  });
    +
    +  it('should load resource from database with router.param()', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    const User = {
    +      findById: async (id: string) => {
    +        if (id === '123') {
    +          return { id: '123', name: 'John' };
    +        }
    +        return null;
    +      }
    +    };
    +
    +    router.param('user', async (id: string, ctx: RouterContext, next: Next) => {
    +      const user = await User.findById(id);
    +
    +      if (!user) {
    +        ctx.throw(404, 'User not found');
    +      }
    +
    +      ctx.state.user = user;
    +      return next();
    +    });
    +
    +    router.get('/users/:user', (ctx: RouterContext) => {
    +      ctx.body = ctx.state.user;
    +    });
    +
    +    router.get('/users/:user/posts', async (ctx: RouterContext) => {
    +      ctx.body = { userId: ctx.state.user.id, posts: [] };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/users/123')
    +      .expect(200);
    +
    +    assert.strictEqual(res1.body.id, '123');
    +    assert.strictEqual(res1.body.name, 'John');
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/users/123/posts')
    +      .expect(200);
    +
    +    assert.strictEqual(res2.body.userId, '123');
    +
    +    await request(http.createServer(app.callback()))
    +      .get('/users/999')
    +      .expect(404);
    +  });
    +
    +  it('should support multiple param handlers', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    let validationCalled = false;
    +    let loadCalled = false;
    +
    +    router.param('id', (value: string, ctx: RouterContext, next: Next) => {
    +      if (!/^\d+$/.test(value)) {
    +        ctx.throw(400, 'Invalid ID format');
    +      }
    +      validationCalled = true;
    +      return next();
    +    });
    +
    +    router.param(
    +      'id',
    +      async (value: string, ctx: RouterContext, next: Next) => {
    +        ctx.state.resource = { id: value, loaded: true };
    +        loadCalled = true;
    +        return next();
    +      }
    +    );
    +
    +    router.get('/resource/:id', (ctx: RouterContext) => {
    +      ctx.body = ctx.state.resource;
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .get('/resource/123')
    +      .expect(200);
    +
    +    assert.strictEqual(validationCalled, true);
    +    assert.strictEqual(loadCalled, true);
    +    assert.strictEqual(res.body.id, '123');
    +    assert.strictEqual(res.body.loaded, true);
    +  });
    +});
    
  • recipes/parameter-validation/parameter-validation.ts+65 0 added
    @@ -0,0 +1,65 @@
    +/**
    + * Parameter Validation with router.param() Recipe
    + *
    + * Validate and transform parameters using router.param().
    + *
    + * Note: User, Post, Resource models are placeholders.
    + * Replace with your actual models/services.
    + */
    +import Router from '../router-module-loader';
    +import { User, Post, Resource, ContextWithUser, Next } from '../common';
    +import type { RouterParameterMiddleware } from '../router-module-loader';
    +
    +const router = new Router();
    +
    +router.param('id', ((value: string, ctx: ContextWithUser, next: Next) => {
    +  const uuidRegex =
    +    /^[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}$/;
    +
    +  if (!uuidRegex.test(value)) {
    +    ctx.throw(400, 'Invalid ID format');
    +  }
    +
    +  return next();
    +}) as RouterParameterMiddleware);
    +
    +router.param('user', (async (id: string, ctx: ContextWithUser, next: Next) => {
    +  const user = await User.findById(id);
    +
    +  if (!user) {
    +    ctx.throw(404, 'User not found');
    +  }
    +
    +  ctx.state.user = user;
    +  return next();
    +}) as RouterParameterMiddleware);
    +
    +router.get('/users/:user', (ctx: ContextWithUser) => {
    +  ctx.body = ctx.state.user;
    +});
    +
    +router.get('/users/:user/posts', async (ctx: ContextWithUser) => {
    +  ctx.body = await Post.findByUserId(ctx.state.user!.id);
    +});
    +
    +router
    +  .param('id', ((value: string, ctx: ContextWithUser, next: Next) => {
    +    if (!/^\d+$/.test(value)) {
    +      ctx.throw(400, 'Invalid ID format');
    +    }
    +    return next();
    +  }) as RouterParameterMiddleware)
    +  .param('id', (async (value: string, ctx: ContextWithUser, next: Next) => {
    +    const resource = await Resource.findById(value);
    +    ctx.state.resource = resource || undefined;
    +    return next();
    +  }) as RouterParameterMiddleware)
    +  .param('id', ((_value: string, ctx: ContextWithUser, next: Next) => {
    +    if (!ctx.state.resource) {
    +      ctx.throw(404);
    +    }
    +    return next();
    +  }) as RouterParameterMiddleware)
    +  .get('/resource/:id', (ctx: ContextWithUser) => {
    +    ctx.body = ctx.state.resource;
    +  });
    
  • recipes/README.md+76 0 added
    @@ -0,0 +1,76 @@
    +# Recipes
    +
    +Common patterns and recipes for building real-world applications with @koa/router.
    +
    +## Available Recipes
    +
    +- **[Nested Routes](./nested-routes/)** - Production-ready nested router patterns with multiple levels, parameter propagation, and real-world examples
    +- **[RESTful API Structure](./restful-api-structure/)** - Organize your API with nested routers for clean separation
    +- **[Authentication & Authorization](./authentication-authorization/)** - Implement JWT-based authentication with middleware
    +- **[Request Validation](./request-validation/)** - Validate request data with middleware
    +- **[Parameter Validation](./parameter-validation/)** - Validate and transform parameters using router.param()
    +- **[API Versioning](./api-versioning/)** - Implement API versioning with multiple routers
    +- **[Error Handling](./error-handling/)** - Centralized error handling with custom error classes
    +- **[Pagination](./pagination/)** - Implement pagination for list endpoints
    +- **[Health Checks](./health-checks/)** - Add health check endpoints for monitoring
    +- **[TypeScript Recipe](./typescript-recipe/)** - Full TypeScript example with types and type safety
    +
    +Each recipe folder contains:
    +
    +- `[recipe-name].ts` - The recipe implementation
    +- `[recipe-name].test.ts` - Comprehensive tests for the recipe
    +
    +## Usage
    +
    +Each recipe file contains complete, runnable TypeScript code examples. You can:
    +
    +1. Copy the code from any recipe file
    +2. Adapt it to your specific needs
    +3. Import and use the patterns in your application
    +
    +## Testing
    +
    +Each recipe includes comprehensive tests alongside the recipe file. Run the tests to see how each pattern works:
    +
    +```bash
    +# Run all recipe tests
    +npm test -- recipes
    +
    +# Run a specific recipe test
    +npm test -- recipes/authentication-authorization/authentication-authorization.test.ts
    +```
    +
    +The tests demonstrate:
    +
    +- How to use each recipe pattern
    +- Expected behavior and responses
    +- Error handling scenarios
    +- Integration with Koa applications
    +
    +## Examples
    +
    +### Using a Recipe
    +
    +```typescript
    +import {
    +  authenticate,
    +  requireRole
    +} from './recipes/authentication-authorization/authentication-authorization';
    +
    +router.get('/admin', authenticate, requireRole('admin'), adminHandler);
    +```
    +
    +### Combining Recipes
    +
    +```typescript
    +import { paginate } from './recipes/pagination/pagination';
    +import { validate } from './recipes/request-validation/request-validation';
    +import { createUserSchema } from './schemas';
    +
    +router.get('/users', paginate, getUsers);
    +router.post('/users', validate(createUserSchema), createUser);
    +```
    +
    +## Contributing
    +
    +If you have a useful recipe pattern, feel free to add it to this directory!
    
  • recipes/request-validation/request-validation.test.ts+112 0 added
    @@ -0,0 +1,112 @@
    +/**
    + * Tests for Request Validation Recipe
    + */
    +
    +import { describe, it } from 'node:test';
    +import * as assert from 'node:assert';
    +import * as http from 'node:http';
    +import Router, { RouterContext } from '../router-module-loader';
    +import request from 'supertest';
    +import Koa from 'koa';
    +import { Next } from '../common';
    +
    +describe('Request Validation', () => {
    +  it('should validate request data with middleware', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    app.use(async (ctx, next) => {
    +      if (ctx.request.is('application/json')) {
    +        let body = '';
    +        for await (const chunk of ctx.req) {
    +          body += chunk;
    +        }
    +        try {
    +          (ctx.request as any).body = JSON.parse(body);
    +        } catch {
    +          (ctx.request as any).body = {};
    +        }
    +      }
    +      await next();
    +    });
    +
    +    const validate =
    +      (schema: any) => async (ctx: RouterContext, next: Next) => {
    +        const body = (ctx.request as any).body || {};
    +        const { error, value } = schema.validate(body, {
    +          abortEarly: false,
    +          stripUnknown: true
    +        });
    +
    +        if (error) {
    +          ctx.status = 400;
    +          ctx.body = {
    +            error: 'Validation failed',
    +            details: error.details.map((d: any) => ({
    +              field: d.path.join('.'),
    +              message: d.message
    +            }))
    +          };
    +          return;
    +        }
    +
    +        (ctx.request as any).body = value;
    +        await next();
    +      };
    +
    +    const createUserSchema = {
    +      validate: (data: any) => {
    +        const errors: any[] = [];
    +        if (!data.email || !data.email.includes('@')) {
    +          errors.push({ path: ['email'], message: 'Email is invalid' });
    +        }
    +        if (!data.password || data.password.length < 8) {
    +          errors.push({
    +            path: ['password'],
    +            message: 'Password must be at least 8 characters'
    +          });
    +        }
    +        if (!data.name || data.name.length < 2) {
    +          errors.push({
    +            path: ['name'],
    +            message: 'Name must be at least 2 characters'
    +          });
    +        }
    +
    +        if (errors.length > 0) {
    +          return { error: { details: errors }, value: null };
    +        }
    +        return { error: null, value: data };
    +      }
    +    };
    +
    +    router.post('/users', validate(createUserSchema), async (ctx: any) => {
    +      ctx.body = { success: true, user: (ctx.request as any).body };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .post('/users')
    +      .send({
    +        email: 'test@example.com',
    +        password: 'password123',
    +        name: 'John Doe'
    +      })
    +      .expect(200);
    +
    +    assert.strictEqual(res1.body.success, true);
    +    assert.strictEqual(res1.body.user.email, 'test@example.com');
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .post('/users')
    +      .send({
    +        email: 'invalid-email',
    +        password: 'short'
    +      })
    +      .expect(400);
    +
    +    assert.strictEqual(res2.body.error, 'Validation failed');
    +    assert.strictEqual(Array.isArray(res2.body.details), true);
    +  });
    +});
    
  • recipes/request-validation/request-validation.ts+62 0 added
    @@ -0,0 +1,62 @@
    +/**
    + * Request Validation Recipe
    + *
    + * Validate request data with middleware.
    + *
    + * Requires: npm install joi
    + */
    +import Router from '../router-module-loader';
    +import * as Joi from 'joi';
    +import { createUser, updateUser, ContextWithBody, Next } from '../common';
    +
    +const router = new Router();
    +
    +const validate =
    +  (schema: Joi.ObjectSchema) => async (ctx: ContextWithBody, next: Next) => {
    +    const { error, value } = schema.validate(ctx.request.body, {
    +      abortEarly: false,
    +      stripUnknown: true
    +    });
    +
    +    if (error) {
    +      ctx.status = 400;
    +      ctx.body = {
    +        error: 'Validation failed',
    +        details: error.details.map((d) => ({
    +          field: d.path.join('.'),
    +          message: d.message
    +        }))
    +      };
    +      return;
    +    }
    +
    +    ctx.request.body = value;
    +    await next();
    +  };
    +
    +const createUserSchema = Joi.object({
    +  email: Joi.string().email().required(),
    +  password: Joi.string().min(8).required(),
    +  name: Joi.string().min(2).required()
    +});
    +
    +const updateUserSchema = Joi.object({
    +  email: Joi.string().email().optional(),
    +  name: Joi.string().min(2).optional()
    +});
    +
    +router.post(
    +  '/users',
    +  validate(createUserSchema),
    +  async (ctx: ContextWithBody) => {
    +    ctx.body = await createUser(ctx.request.body);
    +  }
    +);
    +
    +router.put(
    +  '/users/:id',
    +  validate(updateUserSchema),
    +  async (ctx: ContextWithBody) => {
    +    ctx.body = await updateUser(ctx.params.id, ctx.request.body);
    +  }
    +);
    
  • recipes/restful-api-structure/restful-api-structure.test.ts+92 0 added
    @@ -0,0 +1,92 @@
    +/**
    + * Tests for RESTful API Structure Recipe
    + */
    +
    +import { describe, it } from 'node:test';
    +import * as assert from 'node:assert';
    +import * as http from 'node:http';
    +import Koa from 'koa';
    +import Router from '../router-module-loader';
    +import request from 'supertest';
    +
    +describe('RESTful API Structure', () => {
    +  it('should organize API with nested routers', async () => {
    +    const app = new Koa();
    +
    +    app.use(async (ctx, next) => {
    +      if (ctx.request.is('application/json')) {
    +        let body = '';
    +        for await (const chunk of ctx.req) {
    +          body += chunk;
    +        }
    +        try {
    +          (ctx.request as any).body = JSON.parse(body);
    +        } catch {
    +          (ctx.request as any).body = {};
    +        }
    +      }
    +      await next();
    +    });
    +
    +    const User = {
    +      findAll: async () => [
    +        { id: 1, name: 'John' },
    +        { id: 2, name: 'Jane' }
    +      ],
    +      findById: async (id: string) => ({ id, name: 'John' }),
    +      create: async (data: any) => ({ id: 3, ...data }),
    +      update: async (id: string, data: any) => ({ id, ...data }),
    +      delete: async (_id: string) => true
    +    };
    +
    +    const usersRouter = new Router({ prefix: '/users' });
    +    usersRouter.get('/', async (ctx: any) => {
    +      ctx.body = await User.findAll();
    +    });
    +    usersRouter.post('/', async (ctx: any) => {
    +      ctx.body = await User.create((ctx.request as any).body);
    +    });
    +    usersRouter.get('/:id', async (ctx: any) => {
    +      ctx.body = await User.findById(ctx.params.id);
    +    });
    +    usersRouter.put('/:id', async (ctx: any) => {
    +      ctx.body = await User.update(ctx.params.id, (ctx.request as any).body);
    +    });
    +    usersRouter.delete('/:id', async (ctx: any) => {
    +      await User.delete(ctx.params.id);
    +      ctx.status = 204;
    +    });
    +
    +    const apiRouter = new Router({ prefix: '/api/v1' });
    +    apiRouter.use(usersRouter.routes(), usersRouter.allowedMethods());
    +
    +    app.use(apiRouter.routes());
    +    app.use(apiRouter.allowedMethods());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/api/v1/users')
    +      .expect(200);
    +
    +    assert.strictEqual(Array.isArray(res1.body), true);
    +    assert.strictEqual(res1.body.length, 2);
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/api/v1/users/123')
    +      .expect(200);
    +
    +    assert.strictEqual(res2.body.id, '123');
    +
    +    const res3 = await request(http.createServer(app.callback()))
    +      .post('/api/v1/users')
    +      .send({ name: 'Alice', email: 'alice@example.com' })
    +      .expect(200);
    +
    +    assert.strictEqual(res3.body.name, 'Alice');
    +
    +    const res4 = await request(http.createServer(app.callback()))
    +      .delete('/api/v1/users/123')
    +      .expect(204);
    +
    +    assert.strictEqual(res4.status, 204);
    +  });
    +});
    
  • recipes/restful-api-structure/restful-api-structure.ts+38 0 added
    @@ -0,0 +1,38 @@
    +/**
    + * RESTful API Structure Recipe
    + *
    + * Organize your API with nested routers for clean separation.
    + *
    + * Note: User, Post, and other model references are placeholders.
    + * Replace them with your actual database models or service layer.
    + */
    +import Koa from 'koa';
    +import Router from '../router-module-loader';
    +import { User, ContextWithBody } from '../common';
    +import type { RouterContext } from '../router-module-loader';
    +
    +const app = new Koa();
    +
    +const usersRouter = new Router({ prefix: '/users' });
    +usersRouter.get('/', async (ctx: RouterContext) => {
    +  ctx.body = await User.findAll();
    +});
    +usersRouter.post('/', async (ctx: ContextWithBody) => {
    +  ctx.body = await User.create(ctx.request.body);
    +});
    +usersRouter.get('/:id', async (ctx: RouterContext) => {
    +  ctx.body = await User.findById(ctx.params.id);
    +});
    +usersRouter.put('/:id', async (ctx: ContextWithBody) => {
    +  ctx.body = await User.update(ctx.params.id, ctx.request.body);
    +});
    +usersRouter.delete('/:id', async (ctx: RouterContext) => {
    +  await User.delete(ctx.params.id);
    +  ctx.status = 204;
    +});
    +
    +const apiRouter = new Router({ prefix: '/api/v1' });
    +apiRouter.use(usersRouter.routes(), usersRouter.allowedMethods());
    +
    +app.use(apiRouter.routes());
    +app.use(apiRouter.allowedMethods());
    
  • recipes/router-module-loader.ts+13 0 added
    @@ -0,0 +1,13 @@
    +/**
    + * Router Module Loader
    + *
    + * Centralized import point for router modules.
    + * This allows easy switching between router versions for testing.
    + *
    + * To switch versions, update the import path below:
    + * - For latest and local development: '../src/index'
    + * - For dist build: '../dist/index'
    + * - For published package: '@koa/router'
    + */
    +export { default, default as Router } from '../src/index';
    +export type * from '../src/index';
    
  • recipes/typescript-recipe/typescript-recipe.test.ts+106 0 added
    @@ -0,0 +1,106 @@
    +/**
    + * Tests for TypeScript Recipe
    + */
    +
    +import { describe, it } from 'node:test';
    +import * as assert from 'node:assert';
    +import * as http from 'node:http';
    +import Router, { RouterContext } from '../router-module-loader';
    +import request from 'supertest';
    +import Koa from 'koa';
    +import { Next } from '../common';
    +
    +describe('TypeScript Recipe', () => {
    +  it('should work with typed route handlers', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    app.use(async (ctx, next) => {
    +      if (ctx.request.is('application/json')) {
    +        let body = '';
    +        for await (const chunk of ctx.req) {
    +          body += chunk;
    +        }
    +        try {
    +          (ctx.request as any).body = JSON.parse(body);
    +        } catch {
    +          (ctx.request as any).body = {};
    +        }
    +      }
    +      await next();
    +    });
    +
    +    interface User {
    +      id: number;
    +      name: string;
    +      email: string;
    +    }
    +
    +    const getUserById = async (id: number): Promise<User> => {
    +      return { id, name: 'John Doe', email: 'john@example.com' };
    +    };
    +
    +    const createUser = async (data: {
    +      name: string;
    +      email: string;
    +    }): Promise<User> => {
    +      return { id: 1, ...data };
    +    };
    +
    +    router.get('/users/:id', async (ctx: RouterContext) => {
    +      const userId = parseInt(ctx.params.id, 10);
    +
    +      if (isNaN(userId)) {
    +        ctx.throw(400, 'Invalid user ID');
    +      }
    +
    +      const user: User = await getUserById(userId);
    +      ctx.body = user;
    +    });
    +    interface CreateUserBody {
    +      name: string;
    +      email: string;
    +    }
    +
    +    router.post('/users', async (ctx: RouterContext) => {
    +      const body =
    +        ((ctx.request as any).body as CreateUserBody) || ({} as CreateUserBody);
    +      const { name, email } = body;
    +
    +      const user = await createUser({ name, email });
    +      ctx.status = 201;
    +      ctx.body = user;
    +    });
    +
    +    router.param('id', (value: string, ctx: RouterContext, next: Next) => {
    +      if (!/^\d+$/.test(value)) {
    +        ctx.throw(400, 'Invalid ID');
    +      }
    +      return next();
    +    });
    +
    +    app.use(router.routes());
    +    app.use(router.allowedMethods());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/users/123')
    +      .expect(200);
    +
    +    assert.strictEqual(res1.body.id, 123);
    +    assert.strictEqual(res1.body.name, 'John Doe');
    +    assert.strictEqual(res1.body.email, 'john@example.com');
    +
    +    await request(http.createServer(app.callback()))
    +      .get('/users/abc')
    +      .expect(400);
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .post('/users')
    +      .send({ name: 'Jane Doe', email: 'jane@example.com' })
    +      .expect(201);
    +
    +    assert.strictEqual(res2.body.name, 'Jane Doe');
    +    assert.strictEqual(res2.body.email, 'jane@example.com');
    +    assert.strictEqual(res2.body.id, 1);
    +  });
    +});
    
  • recipes/typescript-recipe/typescript-recipe.ts+61 0 added
    @@ -0,0 +1,61 @@
    +/**
    + * TypeScript Recipe
    + *
    + * Full TypeScript example with types and type safety.
    + *
    + * Note: getUserById and createUser are placeholder functions.
    + * Replace with your actual implementation.
    + */
    +import Router from '../router-module-loader';
    +import { ContextWithBody, Next } from '../common';
    +import type {
    +  RouterContext,
    +  RouterParameterMiddleware
    +} from '../router-module-loader';
    +
    +interface User {
    +  id: number;
    +  name: string;
    +  email: string;
    +}
    +
    +interface CreateUserBody {
    +  name: string;
    +  email: string;
    +}
    +
    +const router = new Router();
    +
    +router.get('/users/:id', async (ctx: RouterContext) => {
    +  const userId = parseInt(ctx.params.id, 10);
    +
    +  if (isNaN(userId)) {
    +    ctx.throw(400, 'Invalid user ID');
    +  }
    +
    +  const user: User = await getUserById(userId);
    +  ctx.body = user;
    +});
    +
    +router.post('/users', async (ctx: ContextWithBody) => {
    +  const { name, email } = (ctx.request.body || {}) as CreateUserBody;
    +
    +  const user = await createUser({ name, email });
    +  ctx.status = 201;
    +  ctx.body = user;
    +});
    +
    +router.param('id', ((value: string, ctx: RouterContext, next: Next) => {
    +  if (!/^\d+$/.test(value)) {
    +    ctx.throw(400, 'Invalid ID');
    +  }
    +  return next();
    +}) as RouterParameterMiddleware);
    +
    +async function getUserById(id: number): Promise<User> {
    +  return { id, name: '', email: '' };
    +}
    +
    +async function createUser(data: CreateUserBody): Promise<User> {
    +  return { id: 1, ...data };
    +}
    
  • .remarkrc.js+0 3 removed
    @@ -1,3 +0,0 @@
    -module.exports = {
    -  plugins: ['preset-github']
    -};
    
  • src/index.ts+20 0 added
    @@ -0,0 +1,20 @@
    +/**
    + * @koa/router - RESTful resource routing middleware for Koa
    + *
    + * @module @koa/router
    + */
    +
    +export type {
    +  RouterOptions,
    +  LayerOptions,
    +  UrlOptions,
    +  RouterParameterMiddleware,
    +  RouterMiddleware,
    +  RouterContext,
    +  MatchResult,
    +  AllowedMethodsOptions,
    +  Layer,
    +  HttpMethod
    +} from './types';
    +
    +export { default, default as Router } from './router';
    
  • src/layer.ts+545 0 added
    @@ -0,0 +1,545 @@
    +import { parse as parseUrl, format as formatUrl } from 'node:url';
    +import type {
    +  LayerOptions,
    +  RouterMiddleware,
    +  RouterParameterMiddleware,
    +  UrlOptions
    +} from './types';
    +import {
    +  compilePathToRegexp,
    +  compilePath,
    +  parsePath,
    +  normalizeLayerOptionsToPathToRegexp,
    +  type Key
    +} from './utils/path-to-regexp-wrapper';
    +
    +/**
    + * Safe decodeURIComponent, won't throw any error.
    + * If `decodeURIComponent` error happen, just return the original value.
    + *
    + * Note: This function is used only for route/path parameters, not query parameters.
    + * In URL path segments, `+` is a literal character (not a space), so we don't
    + * replace `+` with spaces. For query parameters, use a different decoder that
    + * handles `application/x-www-form-urlencoded` format.
    + *
    + * @param text - Text to decode
    + * @returns URL decoded string
    + * @private
    + */
    +function safeDecodeURIComponent(text: string): string {
    +  try {
    +    return decodeURIComponent(text);
    +  } catch {
    +    return text;
    +  }
    +}
    +
    +/**
    + * Extended middleware with param metadata
    + */
    +interface ParameterMiddleware extends Function {
    +  param?: string;
    +  _originalFn?: RouterParameterMiddleware;
    +}
    +
    +export default class Layer {
    +  opts: LayerOptions;
    +  name: string | undefined;
    +  methods: string[];
    +  paramNames: Key[];
    +  stack: (RouterMiddleware | ParameterMiddleware)[];
    +  path: string | RegExp;
    +  regexp!: RegExp;
    +
    +  /**
    +   * Initialize a new routing Layer with given `method`, `path`, and `middleware`.
    +   *
    +   * @param path - Path string or regular expression
    +   * @param methods - Array of HTTP verbs
    +   * @param middleware - Layer callback/middleware or series of
    +   * @param opts - Layer options
    +   * @private
    +   */
    +  constructor(
    +    path: string | RegExp,
    +    methods: string[],
    +    middleware: RouterMiddleware<any, any> | RouterMiddleware<any, any>[],
    +    options: LayerOptions = {}
    +  ) {
    +    this.opts = options;
    +    this.name = this.opts.name || undefined;
    +
    +    this.methods = this._normalizeHttpMethods(methods);
    +
    +    this.stack = this._normalizeAndValidateMiddleware(
    +      middleware,
    +      methods,
    +      path
    +    );
    +
    +    this.path = path;
    +    this.paramNames = [];
    +    this._configurePathMatching();
    +  }
    +
    +  /**
    +   * Normalize HTTP methods and add automatic HEAD support for GET
    +   * @private
    +   */
    +  private _normalizeHttpMethods(methods: string[]): string[] {
    +    const normalizedMethods: string[] = [];
    +
    +    for (const method of methods) {
    +      const upperMethod = method.toUpperCase();
    +      normalizedMethods.push(upperMethod);
    +
    +      if (upperMethod === 'GET') {
    +        normalizedMethods.unshift('HEAD');
    +      }
    +    }
    +
    +    return normalizedMethods;
    +  }
    +
    +  /**
    +   * Normalize middleware to array and validate all are functions
    +   * @private
    +   */
    +  private _normalizeAndValidateMiddleware(
    +    middleware: RouterMiddleware | RouterMiddleware[],
    +    methods: string[],
    +    path: string | RegExp
    +  ): RouterMiddleware[] {
    +    const middlewareArray = Array.isArray(middleware)
    +      ? middleware
    +      : [middleware];
    +
    +    for (const middlewareFunction of middlewareArray) {
    +      const middlewareType = typeof middlewareFunction;
    +
    +      if (middlewareType !== 'function') {
    +        const routeIdentifier = this.opts.name || path;
    +        throw new Error(
    +          `${methods.toString()} \`${routeIdentifier}\`: \`middleware\` must be a function, not \`${middlewareType}\``
    +        );
    +      }
    +    }
    +
    +    return middlewareArray;
    +  }
    +
    +  /**
    +   * Configure path matching regexp and parameters
    +   * @private
    +   */
    +  private _configurePathMatching(): void {
    +    if (this.opts.pathAsRegExp === true) {
    +      this.regexp =
    +        this.path instanceof RegExp
    +          ? this.path
    +          : new RegExp(this.path as string);
    +    } else if (this.path) {
    +      this._configurePathToRegexp();
    +    }
    +  }
    +
    +  /**
    +   * Configure path-to-regexp for string paths
    +   * @private
    +   */
    +  private _configurePathToRegexp(): void {
    +    const options = normalizeLayerOptionsToPathToRegexp(this.opts);
    +    const { regexp, keys } = compilePathToRegexp(this.path as string, options);
    +    this.regexp = regexp;
    +    this.paramNames = keys;
    +  }
    +
    +  /**
    +   * Returns whether request `path` matches route.
    +   *
    +   * @param path - Request path
    +   * @returns Whether path matches
    +   * @private
    +   */
    +  match(path: string): boolean {
    +    return this.regexp.test(path);
    +  }
    +
    +  /**
    +   * Returns map of URL parameters for given `path` and `paramNames`.
    +   *
    +   * @param _path - Request path (not used, kept for API compatibility)
    +   * @param captures - Captured values from regexp
    +   * @param existingParams - Existing params to merge with
    +   * @returns Parameter map
    +   * @private
    +   */
    +  params(
    +    _path: string,
    +    captures: string[],
    +    existingParameters: Record<string, string> = {}
    +  ): Record<string, string> {
    +    const parameterValues = { ...existingParameters };
    +
    +    for (const [captureIndex, capturedValue] of captures.entries()) {
    +      const parameterDefinition = this.paramNames[captureIndex];
    +
    +      if (parameterDefinition && capturedValue && capturedValue.length > 0) {
    +        const parameterName = parameterDefinition.name;
    +        parameterValues[parameterName] = safeDecodeURIComponent(capturedValue);
    +      }
    +    }
    +
    +    return parameterValues;
    +  }
    +
    +  /**
    +   * Returns array of regexp url path captures.
    +   *
    +   * @param path - Request path
    +   * @returns Array of captured values
    +   * @private
    +   */
    +  captures(path: string): string[] {
    +    if (this.opts.ignoreCaptures) {
    +      return [];
    +    }
    +
    +    const match = path.match(this.regexp);
    +    return match ? match.slice(1) : [];
    +  }
    +
    +  /**
    +   * Generate URL for route using given `params`.
    +   *
    +   * @example
    +   *
    +   * ```javascript
    +   * const route = new Layer('/users/:id', ['GET'], fn);
    +   *
    +   * route.url({ id: 123 }); // => "/users/123"
    +   * ```
    +   *
    +   * @param args - URL parameters (various formats supported)
    +   * @returns Generated URL
    +   * @private
    +   */
    +  url(...arguments_: any[]): string {
    +    const { params, options } = this._parseUrlArguments(arguments_);
    +
    +    const cleanPath = (this.path as string).replaceAll('(.*)', '');
    +
    +    const pathCompiler = compilePath(cleanPath, {
    +      encode: encodeURIComponent,
    +      ...options
    +    });
    +
    +    const parameterReplacements = this._buildParamReplacements(
    +      params,
    +      cleanPath
    +    );
    +
    +    const generatedUrl = pathCompiler(parameterReplacements);
    +
    +    if (options && options.query) {
    +      return this._addQueryString(generatedUrl, options.query);
    +    }
    +
    +    return generatedUrl;
    +  }
    +
    +  /**
    +   * Parse url() arguments into params and options
    +   * Supports multiple call signatures:
    +   * - url({ id: 1 })
    +   * - url(1, 2, 3)
    +   * - url({ query: {...} })
    +   * - url({ id: 1 }, { query: {...} })
    +   * @private
    +   */
    +  private _parseUrlArguments(allArguments: any[]): {
    +    params: any;
    +    options?: UrlOptions;
    +  } {
    +    let parameters: any = allArguments[0];
    +    let options: UrlOptions | undefined = allArguments[1];
    +
    +    if (typeof parameters !== 'object') {
    +      const argumentsList = [...allArguments];
    +      const lastArgument = argumentsList.at(-1);
    +
    +      if (typeof lastArgument === 'object') {
    +        options = lastArgument;
    +        parameters = argumentsList.slice(0, -1);
    +      } else {
    +        parameters = argumentsList;
    +      }
    +    } else if (parameters && parameters.query && !options) {
    +      options = parameters;
    +      parameters = {};
    +    }
    +
    +    return { params: parameters, options };
    +  }
    +
    +  /**
    +   * Build parameter replacements for URL generation
    +   * @private
    +   */
    +  private _buildParamReplacements(
    +    parameters: any,
    +    cleanPath: string
    +  ): Record<string, string> {
    +    const { tokens } = parsePath(cleanPath);
    +    const hasNamedParameters = tokens.some(
    +      (token) => 'name' in token && token.name
    +    );
    +    const parameterReplacements: Record<string, string> = {};
    +
    +    if (Array.isArray(parameters)) {
    +      let parameterIndex = 0;
    +
    +      for (const token of tokens) {
    +        if ('name' in token && token.name) {
    +          parameterReplacements[token.name] = String(
    +            parameters[parameterIndex++]
    +          );
    +        }
    +      }
    +    } else if (
    +      hasNamedParameters &&
    +      typeof parameters === 'object' &&
    +      !parameters.query
    +    ) {
    +      for (const [parameterName, parameterValue] of Object.entries(
    +        parameters
    +      )) {
    +        parameterReplacements[parameterName] = String(parameterValue);
    +      }
    +    }
    +
    +    return parameterReplacements;
    +  }
    +
    +  /**
    +   * Add query string to URL
    +   * @private
    +   */
    +  private _addQueryString(
    +    baseUrl: string,
    +    query: Record<string, any> | string
    +  ): string {
    +    const parsedUrl: any = parseUrl(baseUrl);
    +
    +    if (typeof query === 'string') {
    +      parsedUrl.search = query;
    +    } else {
    +      parsedUrl.search = undefined;
    +      parsedUrl.query = query;
    +    }
    +
    +    return formatUrl(parsedUrl);
    +  }
    +
    +  /**
    +   * Run validations on route named parameters.
    +   *
    +   * @example
    +   *
    +   * ```javascript
    +   * router
    +   *   .param('user', function (id, ctx, next) {
    +   *     ctx.user = users[id];
    +   *     if (!ctx.user) return ctx.status = 404;
    +   *     next();
    +   *   })
    +   *   .get('/users/:user', function (ctx, next) {
    +   *     ctx.body = ctx.user;
    +   *   });
    +   * ```
    +   *
    +   * @param paramName - Parameter name
    +   * @param paramHandler - Middleware function
    +   * @returns This layer instance
    +   * @private
    +   */
    +  param(
    +    parameterName: string,
    +    parameterHandler: RouterParameterMiddleware
    +  ): Layer {
    +    const middlewareStack = this.stack;
    +    const routeParameterNames = this.paramNames;
    +
    +    const parameterMiddleware = this._createParamMiddleware(
    +      parameterName,
    +      parameterHandler
    +    );
    +
    +    const parameterNamesList = routeParameterNames.map(
    +      (parameterDefinition) => parameterDefinition.name
    +    );
    +
    +    const parameterPosition = parameterNamesList.indexOf(parameterName);
    +
    +    if (parameterPosition !== -1) {
    +      this._insertParamMiddleware(
    +        middlewareStack,
    +        parameterMiddleware,
    +        parameterNamesList,
    +        parameterPosition
    +      );
    +    }
    +
    +    return this;
    +  }
    +
    +  /**
    +   * Create param middleware with deduplication tracking
    +   * @private
    +   */
    +  private _createParamMiddleware(
    +    parameterName: string,
    +    parameterHandler: RouterParameterMiddleware
    +  ): ParameterMiddleware {
    +    const middleware: ParameterMiddleware = function (
    +      this: any,
    +      context: any,
    +      next: () => Promise<any>
    +    ) {
    +      if (!context._matchedParams) {
    +        context._matchedParams = new WeakMap();
    +      }
    +
    +      if (context._matchedParams.has(parameterHandler)) {
    +        return next();
    +      }
    +
    +      context._matchedParams.set(parameterHandler, true);
    +
    +      return parameterHandler.call(
    +        this,
    +        context.params[parameterName],
    +        context,
    +        next
    +      );
    +    };
    +
    +    middleware.param = parameterName;
    +    middleware._originalFn = parameterHandler;
    +
    +    return middleware;
    +  }
    +
    +  /**
    +   * Insert param middleware at the correct position in the stack
    +   * @private
    +   */
    +  private _insertParamMiddleware(
    +    middlewareStack: (RouterMiddleware | ParameterMiddleware)[],
    +    parameterMiddleware: ParameterMiddleware,
    +    parameterNamesList: string[],
    +    currentParameterPosition: number
    +  ): void {
    +    middlewareStack.some((existingMiddleware: any, stackIndex) => {
    +      if (!existingMiddleware.param) {
    +        middlewareStack.splice(stackIndex, 0, parameterMiddleware);
    +        return true;
    +      }
    +
    +      const existingParameterPosition = parameterNamesList.indexOf(
    +        existingMiddleware.param
    +      );
    +      if (existingParameterPosition > currentParameterPosition) {
    +        middlewareStack.splice(stackIndex, 0, parameterMiddleware);
    +        return true;
    +      }
    +
    +      return false;
    +    });
    +  }
    +
    +  /**
    +   * Prefix route path.
    +   *
    +   * @param prefixPath - Prefix to prepend
    +   * @returns This layer instance
    +   * @private
    +   */
    +  setPrefix(prefixPath: string): Layer {
    +    if (!this.path) {
    +      return this;
    +    }
    +
    +    if (this.path instanceof RegExp) {
    +      return this;
    +    }
    +
    +    this.path = this._applyPrefix(prefixPath);
    +
    +    this._reconfigurePathMatching(prefixPath);
    +
    +    return this;
    +  }
    +
    +  /**
    +   * Apply prefix to the current path
    +   * @private
    +   */
    +  private _applyPrefix(prefixPath: string): string {
    +    const isRootPath = this.path === '/';
    +    const isStrictMode = this.opts.strict === true;
    +    const prefixHasParameters = prefixPath.includes(':');
    +    const pathIsRawRegex =
    +      this.opts.pathAsRegExp === true && typeof this.path === 'string';
    +
    +    if (prefixHasParameters && pathIsRawRegex) {
    +      const currentPath = this.path as string;
    +      if (
    +        currentPath === String.raw`(?:\/|$)` ||
    +        currentPath === String.raw`(?:\/|$)`
    +      ) {
    +        this.path = '{/*rest}';
    +        this.opts.pathAsRegExp = false;
    +      }
    +    }
    +
    +    if (isRootPath && !isStrictMode) {
    +      return prefixPath;
    +    }
    +
    +    return `${prefixPath}${this.path}`;
    +  }
    +
    +  /**
    +   * Reconfigure path matching after prefix is applied
    +   * @private
    +   */
    +  private _reconfigurePathMatching(prefixPath: string): void {
    +    const treatAsRegExp = this.opts.pathAsRegExp === true;
    +    const prefixHasParameters = prefixPath && prefixPath.includes(':');
    +
    +    if (prefixHasParameters && treatAsRegExp) {
    +      const options = normalizeLayerOptionsToPathToRegexp(this.opts);
    +      const { regexp, keys } = compilePathToRegexp(
    +        this.path as string,
    +        options
    +      );
    +      this.regexp = regexp;
    +      this.paramNames = keys;
    +      this.opts.pathAsRegExp = false;
    +    } else if (treatAsRegExp) {
    +      this.regexp =
    +        this.path instanceof RegExp
    +          ? this.path
    +          : new RegExp(this.path as string);
    +    } else {
    +      const options = normalizeLayerOptionsToPathToRegexp(this.opts);
    +      const { regexp, keys } = compilePathToRegexp(
    +        this.path as string,
    +        options
    +      );
    +      this.regexp = regexp;
    +      this.paramNames = keys;
    +    }
    +  }
    +}
    
  • src/router.ts+1301 0 added
    @@ -0,0 +1,1301 @@
    +import debugModule from 'debug';
    +const debug = debugModule('koa-router');
    +
    +import compose from 'koa-compose';
    +import HttpError from 'http-errors';
    +import type { Middleware, ParameterizedContext } from 'koa';
    +
    +import Layer from './layer';
    +import { getAllHttpMethods, COMMON_HTTP_METHODS } from './utils/http-methods';
    +import {
    +  applyAllParameterMiddleware,
    +  applyParameterMiddlewareToRoute
    +} from './utils/parameter-helpers';
    +import {
    +  hasPathParameters,
    +  determineMiddlewarePath
    +} from './utils/path-helpers';
    +
    +import type {
    +  RouterOptions,
    +  RouterMiddleware,
    +  RouterParameterMiddleware,
    +  RouterContext,
    +  RouterParameterContext,
    +  MatchResult,
    +  AllowedMethodsOptions,
    +  LayerOptions
    +} from './types';
    +
    +const httpMethods = getAllHttpMethods();
    +
    +/**
    + * Middleware with router property
    + */
    +interface RouterComposedMiddleware<
    +  StateT = import('koa').DefaultState,
    +  ContextT = import('koa').DefaultContext
    +> extends Middleware<
    +  StateT,
    +  ContextT & RouterParameterContext<StateT, ContextT>
    +> {
    +  router?: Router<StateT, ContextT>;
    +}
    +
    +/**
    + * @module koa-router
    + */
    +export default class Router<
    +  StateT = import('koa').DefaultState,
    +  ContextT = import('koa').DefaultContext
    +> {
    +  opts: RouterOptions;
    +  methods: string[];
    +  exclusive: boolean;
    +  params: Record<
    +    string,
    +    | RouterParameterMiddleware<StateT, ContextT>
    +    | RouterParameterMiddleware<StateT, ContextT>[]
    +  >;
    +  stack: Layer[];
    +  host?: string | string[] | RegExp;
    +
    +  /**
    +   * Create a new router.
    +   *
    +   * @example
    +   *
    +   * Basic usage:
    +   *
    +   * ```javascript
    +   * const Koa = require('koa');
    +   * const Router = require('@koa/router');
    +   *
    +   * const app = new Koa();
    +   * const router = new Router();
    +   *
    +   * router.get('/', (ctx, next) => {
    +   *   // ctx.router available
    +   * });
    +   *
    +   * app
    +   *   .use(router.routes())
    +   *   .use(router.allowedMethods());
    +   * ```
    +   *
    +   * @alias module:koa-router
    +   * @param opts - Router options
    +   * @constructor
    +   */
    +  constructor(options: RouterOptions = {}) {
    +    this.opts = options;
    +    this.methods = this.opts.methods || [
    +      'HEAD',
    +      'OPTIONS',
    +      'GET',
    +      'PUT',
    +      'PATCH',
    +      'POST',
    +      'DELETE'
    +    ];
    +    this.exclusive = Boolean(this.opts.exclusive);
    +
    +    this.params = {};
    +    this.stack = [];
    +    this.host = this.opts.host;
    +  }
    +
    +  /**
    +   * Generate URL from url pattern and given `params`.
    +   *
    +   * @example
    +   *
    +   * ```javascript
    +   * const url = Router.url('/users/:id', {id: 1});
    +   * // => "/users/1"
    +   * ```
    +   *
    +   * @param path - URL pattern
    +   * @param args - URL parameters
    +   * @returns Generated URL
    +   */
    +  static url(path: string | RegExp, ...arguments_: any[]): string {
    +    const temporaryLayer = new Layer(path, [], () => {});
    +    return temporaryLayer.url(...arguments_);
    +  }
    +
    +  /**
    +   * Use given middleware.
    +   *
    +   * Middleware run in the order they are defined by `.use()`. They are invoked
    +   * sequentially, requests start at the first middleware and work their way
    +   * "down" the middleware stack.
    +   *
    +   * @example
    +   *
    +   * ```javascript
    +   * // session middleware will run before authorize
    +   * router
    +   *   .use(session())
    +   *   .use(authorize());
    +   *
    +   * // use middleware only with given path
    +   * router.use('/users', userAuth());
    +   *
    +   * // or with an array of paths
    +   * router.use(['/users', '/admin'], userAuth());
    +   *
    +   * app.use(router.routes());
    +   * ```
    +   *
    +   * @param middleware - Middleware functions
    +   * @returns This router instance
    +   */
    +  use(
    +    ...middleware: Array<
    +      | RouterMiddleware<StateT, ContextT>
    +      | RouterComposedMiddleware<StateT, ContextT>
    +    >
    +  ): Router<StateT, ContextT>;
    +  use(
    +    path: string | RegExp | string[],
    +    ...middleware: Array<
    +      | RouterMiddleware<StateT, ContextT>
    +      | RouterComposedMiddleware<StateT, ContextT>
    +    >
    +  ): Router<StateT, ContextT>;
    +  use(
    +    ...middleware: (
    +      | string
    +      | RegExp
    +      | string[]
    +      | RouterMiddleware<StateT, ContextT>
    +      | RouterComposedMiddleware<StateT, ContextT>
    +    )[]
    +  ): Router<StateT, ContextT> {
    +    let explicitPath: string | RegExp | undefined;
    +
    +    if (this._isPathArray(middleware[0])) {
    +      return this._useWithPathArray(middleware);
    +    }
    +
    +    const hasExplicitPath = this._hasExplicitPath(middleware[0]);
    +    if (hasExplicitPath) {
    +      explicitPath = middleware.shift() as string | RegExp;
    +    }
    +
    +    for (const currentMiddleware of middleware) {
    +      if (this._isNestedRouter(currentMiddleware)) {
    +        this._mountNestedRouter(
    +          currentMiddleware as RouterComposedMiddleware<StateT, ContextT>,
    +          explicitPath
    +        );
    +      } else {
    +        this._registerMiddleware(
    +          currentMiddleware as RouterMiddleware<StateT, ContextT>,
    +          explicitPath,
    +          hasExplicitPath
    +        );
    +      }
    +    }
    +
    +    return this;
    +  }
    +
    +  /**
    +   * Check if first argument is an array of paths
    +   * @private
    +   */
    +  private _isPathArray(firstArgument: any): firstArgument is string[] {
    +    return Array.isArray(firstArgument) && typeof firstArgument[0] === 'string';
    +  }
    +
    +  /**
    +   * Check if first argument is an explicit path (string or RegExp)
    +   * Empty string counts as explicit path to enable param capture
    +   * @private
    +   */
    +  private _hasExplicitPath(firstArgument: any): boolean {
    +    return typeof firstArgument === 'string' || firstArgument instanceof RegExp;
    +  }
    +
    +  /**
    +   * Check if middleware contains a nested router
    +   * @private
    +   */
    +  private _isNestedRouter(
    +    middleware: any
    +  ): middleware is RouterComposedMiddleware<StateT, ContextT> {
    +    return middleware.router !== undefined;
    +  }
    +
    +  /**
    +   * Apply middleware to multiple paths
    +   * @private
    +   */
    +  private _useWithPathArray(middleware: any[]): Router<StateT, ContextT> {
    +    const pathArray = middleware[0] as string[];
    +    const remainingMiddleware = middleware.slice(1);
    +
    +    for (const singlePath of pathArray) {
    +      Reflect.apply(this.use, this, [singlePath, ...remainingMiddleware]);
    +    }
    +
    +    return this;
    +  }
    +
    +  /**
    +   * Mount a nested router
    +   * @private
    +   */
    +  private _mountNestedRouter(
    +    middlewareWithRouter: RouterComposedMiddleware<StateT, ContextT>,
    +    mountPath?: string | RegExp
    +  ): void {
    +    const nestedRouter = middlewareWithRouter.router!;
    +
    +    const clonedRouter = this._cloneRouter(nestedRouter);
    +
    +    const mountPathHasParameters =
    +      mountPath &&
    +      typeof mountPath === 'string' &&
    +      hasPathParameters(mountPath, this.opts);
    +
    +    for (
    +      let routeIndex = 0;
    +      routeIndex < clonedRouter.stack.length;
    +      routeIndex++
    +    ) {
    +      const nestedLayer = clonedRouter.stack[routeIndex];
    +      const clonedLayer = this._cloneLayer(nestedLayer);
    +
    +      if (mountPath && typeof mountPath === 'string') {
    +        clonedLayer.setPrefix(mountPath);
    +      }
    +      if (this.opts.prefix) {
    +        clonedLayer.setPrefix(this.opts.prefix);
    +      }
    +
    +      if (clonedLayer.methods.length === 0 && mountPathHasParameters) {
    +        clonedLayer.opts.ignoreCaptures = false;
    +      }
    +
    +      this.stack.push(clonedLayer);
    +      clonedRouter.stack[routeIndex] = clonedLayer;
    +    }
    +
    +    if (this.params) {
    +      this._applyParamMiddlewareToRouter(clonedRouter as any);
    +    }
    +  }
    +
    +  /**
    +   * Clone a router instance
    +   * @private
    +   */
    +  private _cloneRouter(
    +    sourceRouter: Router<StateT, ContextT>
    +  ): Router<StateT, ContextT> {
    +    return Object.assign(
    +      Object.create(Object.getPrototypeOf(sourceRouter)),
    +      sourceRouter,
    +      {
    +        stack: [...sourceRouter.stack]
    +      }
    +    );
    +  }
    +
    +  /**
    +   * Clone a layer instance
    +   * @private
    +   */
    +  private _cloneLayer(sourceLayer: Layer): Layer {
    +    return Object.assign(
    +      Object.create(Object.getPrototypeOf(sourceLayer)),
    +      sourceLayer
    +    );
    +  }
    +
    +  /**
    +   * Apply this router's param middleware to a nested router
    +   * @private
    +   */
    +  private _applyParamMiddlewareToRouter(targetRouter: Router): void {
    +    const parameterNames = Object.keys(this.params);
    +
    +    for (const parameterName of parameterNames) {
    +      const parameterMiddleware = this.params[parameterName];
    +      applyParameterMiddlewareToRoute(
    +        targetRouter as any,
    +        parameterName,
    +        parameterMiddleware as any
    +      );
    +    }
    +  }
    +
    +  /**
    +   * Register regular middleware (not nested router)
    +   * @private
    +   */
    +  private _registerMiddleware(
    +    middleware: RouterMiddleware<StateT, ContextT>,
    +    explicitPath?: string | RegExp,
    +    hasExplicitPath?: boolean
    +  ): void {
    +    const prefixHasParameters = hasPathParameters(
    +      this.opts.prefix || '',
    +      this.opts
    +    );
    +
    +    const effectiveExplicitPath = (() => {
    +      if (explicitPath !== undefined) return explicitPath;
    +      if (prefixHasParameters) return '';
    +      return;
    +    })();
    +
    +    const effectiveHasExplicitPath =
    +      hasExplicitPath || (explicitPath === undefined && prefixHasParameters);
    +
    +    const { path: middlewarePath, pathAsRegExp } = determineMiddlewarePath(
    +      effectiveExplicitPath,
    +      prefixHasParameters
    +    );
    +
    +    let finalPath: string | RegExp = middlewarePath;
    +    let usePathToRegexp = pathAsRegExp;
    +
    +    const isRootPath = effectiveHasExplicitPath && middlewarePath === '/';
    +
    +    if (effectiveHasExplicitPath && typeof middlewarePath === 'string') {
    +      finalPath = middlewarePath;
    +      usePathToRegexp = false;
    +    }
    +
    +    this.register(finalPath, [], middleware, {
    +      end: isRootPath,
    +      ignoreCaptures: !effectiveHasExplicitPath && !prefixHasParameters,
    +      pathAsRegExp: usePathToRegexp
    +    });
    +  }
    +
    +  /**
    +   * Set the path prefix for a Router instance that was already initialized.
    +   *
    +   * @example
    +   *
    +   * ```javascript
    +   * router.prefix('/things/:thing_id')
    +   * ```
    +   *
    +   * @param prefixPath - Prefix string
    +   * @returns This router instance
    +   */
    +  prefix(prefixPath: string): Router<StateT, ContextT> {
    +    const normalizedPrefix = prefixPath.replace(/\/$/, '');
    +
    +    this.opts.prefix = normalizedPrefix;
    +
    +    for (const route of this.stack) {
    +      route.setPrefix(normalizedPrefix);
    +    }
    +
    +    return this;
    +  }
    +
    +  /**
    +   * Returns router middleware which dispatches a route matching the request.
    +   *
    +   * @returns Router middleware
    +   */
    +  middleware(): RouterComposedMiddleware<StateT, ContextT> {
    +    const dispatchMiddleware = function (
    +      this: Router<StateT, ContextT>,
    +      context: ParameterizedContext<
    +        StateT,
    +        ContextT & RouterParameterContext<StateT, ContextT>
    +      >,
    +      next: () => Promise<any>
    +    ) {
    +      debug('%s %s', context.method, context.path);
    +
    +      if (!this.matchHost(context.host)) {
    +        return next();
    +      }
    +
    +      const requestPath = this._getRequestPath(context);
    +
    +      const matchResult = this.match(requestPath, context.method);
    +
    +      this._storeMatchedRoutes(context, matchResult);
    +      context.router = this;
    +
    +      if (!matchResult.route) {
    +        return next();
    +      }
    +
    +      const matchedLayers = matchResult.pathAndMethod;
    +      this._setMatchedRouteInfo(context, matchedLayers);
    +
    +      const middlewareChain = this._buildMiddlewareChain(
    +        matchedLayers,
    +        requestPath
    +      );
    +      return compose(middlewareChain)(context, next);
    +    }.bind(this);
    +
    +    (dispatchMiddleware as RouterComposedMiddleware<StateT, ContextT>).router =
    +      this;
    +    return dispatchMiddleware as RouterComposedMiddleware<StateT, ContextT>;
    +  }
    +
    +  /**
    +   * Get the request path to use for routing
    +   * @private
    +   */
    +  private _getRequestPath(context: RouterContext<StateT, ContextT>): string {
    +    return (
    +      this.opts.routerPath ||
    +      context.newRouterPath ||
    +      context.path ||
    +      context.routerPath ||
    +      ''
    +    );
    +  }
    +
    +  /**
    +   * Store matched routes on context
    +   * @private
    +   */
    +  private _storeMatchedRoutes(
    +    context: RouterContext<StateT, ContextT>,
    +    matchResult: MatchResult
    +  ): void {
    +    if (context.matched) {
    +      context.matched.push(...matchResult.path);
    +    } else {
    +      context.matched = matchResult.path;
    +    }
    +  }
    +
    +  /**
    +   * Set matched route information on context
    +   * @private
    +   */
    +  private _setMatchedRouteInfo(
    +    context: RouterContext<StateT, ContextT>,
    +    matchedLayers: Layer[]
    +  ): void {
    +    const routeLayer = matchedLayers
    +      .toReversed()
    +      .find((layer: Layer) => layer.methods.length > 0);
    +
    +    if (routeLayer) {
    +      context._matchedRoute = routeLayer.path as string;
    +
    +      if (routeLayer.name) {
    +        context._matchedRouteName = routeLayer.name;
    +      }
    +    }
    +  }
    +
    +  /**
    +   * Build middleware chain from matched layers
    +   * @private
    +   */
    +  private _buildMiddlewareChain(
    +    matchedLayers: Layer[],
    +    requestPath: string
    +  ): RouterMiddleware<StateT, ContextT>[] {
    +    const layersToExecute = this.opts.exclusive
    +      ? [matchedLayers.at(-1)].filter(
    +          (layer): layer is Layer => layer !== undefined
    +        )
    +      : matchedLayers;
    +
    +    const middlewareChain: RouterMiddleware<StateT, ContextT>[] = [];
    +
    +    for (const layer of layersToExecute) {
    +      middlewareChain.push(
    +        (
    +          context: ParameterizedContext<
    +            StateT,
    +            ContextT & RouterParameterContext<StateT, ContextT>
    +          >,
    +          next: () => Promise<any>
    +        ) => {
    +          const routerContext = context as RouterContext<StateT, ContextT>;
    +          routerContext.captures = layer.captures(requestPath);
    +          routerContext.request.params = layer.params(
    +            requestPath,
    +            routerContext.captures || [],
    +            routerContext.params
    +          );
    +          routerContext.params = routerContext.request.params;
    +          routerContext.routerPath = layer.path as string;
    +          routerContext.routerName = layer.name || undefined;
    +          routerContext._matchedRoute = layer.path as string;
    +
    +          if (layer.name) {
    +            routerContext._matchedRouteName = layer.name;
    +          }
    +
    +          return next();
    +        },
    +        ...(layer.stack as RouterMiddleware<StateT, ContextT>[])
    +      );
    +    }
    +
    +    return middlewareChain;
    +  }
    +
    +  routes(): RouterComposedMiddleware<StateT, ContextT> {
    +    return this.middleware();
    +  }
    +
    +  /**
    +   * Returns separate middleware for responding to `OPTIONS` requests with
    +   * an `Allow` header containing the allowed methods, as well as responding
    +   * with `405 Method Not Allowed` and `501 Not Implemented` as appropriate.
    +   *
    +   * @example
    +   *
    +   * ```javascript
    +   * const Koa = require('koa');
    +   * const Router = require('@koa/router');
    +   *
    +   * const app = new Koa();
    +   * const router = new Router();
    +   *
    +   * app.use(router.routes());
    +   * app.use(router.allowedMethods());
    +   * ```
    +   *
    +   * **Example with [Boom](https://github.com/hapijs/boom)**
    +   *
    +   * ```javascript
    +   * const Koa = require('koa');
    +   * const Router = require('@koa/router');
    +   * const Boom = require('boom');
    +   *
    +   * const app = new Koa();
    +   * const router = new Router();
    +   *
    +   * app.use(router.routes());
    +   * app.use(router.allowedMethods({
    +   *   throw: true,
    +   *   notImplemented: () => new Boom.notImplemented(),
    +   *   methodNotAllowed: () => new Boom.methodNotAllowed()
    +   * }));
    +   * ```
    +   *
    +   * @param options - Options object
    +   * @returns Middleware function
    +   */
    +  allowedMethods(
    +    options: AllowedMethodsOptions = {}
    +  ): RouterMiddleware<StateT, ContextT> {
    +    const implementedMethods = this.methods;
    +
    +    return (
    +      context: ParameterizedContext<
    +        StateT,
    +        ContextT & RouterParameterContext<StateT, ContextT>
    +      >,
    +      next: () => Promise<any>
    +    ) => {
    +      const routerContext = context as RouterContext<StateT, ContextT>;
    +      return next().then(() => {
    +        if (!this._shouldProcessAllowedMethods(routerContext)) {
    +          return;
    +        }
    +
    +        const allowedMethods = this._collectAllowedMethods(
    +          routerContext.matched!
    +        );
    +        const allowedMethodsList = Object.keys(allowedMethods);
    +
    +        if (!implementedMethods.includes(context.method)) {
    +          this._handleNotImplemented(
    +            routerContext,
    +            allowedMethodsList,
    +            options
    +          );
    +          return;
    +        }
    +
    +        if (context.method === 'OPTIONS' && allowedMethodsList.length > 0) {
    +          this._handleOptionsRequest(routerContext, allowedMethodsList);
    +          return;
    +        }
    +
    +        if (allowedMethodsList.length > 0 && !allowedMethods[context.method]) {
    +          this._handleMethodNotAllowed(
    +            routerContext,
    +            allowedMethodsList,
    +            options
    +          );
    +        }
    +      });
    +    };
    +  }
    +
    +  /**
    +   * Check if we should process allowed methods
    +   * @private
    +   */
    +  private _shouldProcessAllowedMethods(
    +    context: RouterContext<any, any>
    +  ): boolean {
    +    return !!(context.matched && (!context.status || context.status === 404));
    +  }
    +
    +  /**
    +   * Collect all allowed methods from matched routes
    +   * @private
    +   */
    +  private _collectAllowedMethods(
    +    matchedRoutes: Layer[]
    +  ): Record<string, string> {
    +    const allowedMethods: Record<string, string> = {};
    +
    +    for (const route of matchedRoutes) {
    +      for (const method of route.methods) {
    +        allowedMethods[method] = method;
    +      }
    +    }
    +
    +    return allowedMethods;
    +  }
    +
    +  /**
    +   * Handle 501 Not Implemented response
    +   * @private
    +   */
    +  private _handleNotImplemented(
    +    context: RouterContext<any, any>,
    +    allowedMethodsList: string[],
    +    options: AllowedMethodsOptions
    +  ): void {
    +    if (options.throw) {
    +      const error =
    +        typeof options.notImplemented === 'function'
    +          ? options.notImplemented()
    +          : new HttpError.NotImplemented();
    +      throw error;
    +    }
    +
    +    context.status = 501;
    +    context.set('Allow', allowedMethodsList.join(', '));
    +  }
    +
    +  /**
    +   * Handle OPTIONS request
    +   * @private
    +   */
    +  private _handleOptionsRequest(
    +    context: RouterContext<any, any>,
    +    allowedMethodsList: string[]
    +  ): void {
    +    context.status = 200;
    +    context.body = '';
    +    context.set('Allow', allowedMethodsList.join(', '));
    +  }
    +
    +  /**
    +   * Handle 405 Method Not Allowed response
    +   * @private
    +   */
    +  private _handleMethodNotAllowed(
    +    context: RouterContext<any, any>,
    +    allowedMethodsList: string[],
    +    options: AllowedMethodsOptions
    +  ): void {
    +    if (options.throw) {
    +      const error =
    +        typeof options.methodNotAllowed === 'function'
    +          ? options.methodNotAllowed()
    +          : new HttpError.MethodNotAllowed();
    +      throw error;
    +    }
    +
    +    context.status = 405;
    +    context.set('Allow', allowedMethodsList.join(', '));
    +  }
    +
    +  /**
    +   * Register route with all methods.
    +   *
    +   * @param args - Route arguments (name, path, middleware)
    +   * @returns This router instance
    +   */
    +  all<T = {}, U = {}, B = unknown>(
    +    name: string,
    +    path: string | RegExp,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  all<T = {}, U = {}, B = unknown>(
    +    path: string | RegExp | Array<string | RegExp>,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  all(...arguments_: any[]): Router<StateT, ContextT> {
    +    let name: string | undefined;
    +    let path: string | RegExp | string[];
    +    let middleware: RouterMiddleware<StateT, ContextT>[];
    +
    +    if (
    +      arguments_.length >= 2 &&
    +      (typeof arguments_[1] === 'string' || arguments_[1] instanceof RegExp)
    +    ) {
    +      name = arguments_[0];
    +      path = arguments_[1];
    +      middleware = arguments_.slice(2);
    +    } else {
    +      name = undefined;
    +      path = arguments_[0];
    +      middleware = arguments_.slice(1);
    +    }
    +
    +    if (
    +      typeof path !== 'string' &&
    +      !(path instanceof RegExp) &&
    +      (!Array.isArray(path) || path.length === 0)
    +    )
    +      throw new Error('You have to provide a path when adding an all handler');
    +
    +    const routeOptions: LayerOptions = {
    +      name,
    +      pathAsRegExp: path instanceof RegExp
    +    };
    +
    +    this.register(path, httpMethods, middleware, {
    +      ...this.opts,
    +      ...routeOptions
    +    });
    +
    +    return this;
    +  }
    +
    +  /**
    +   * Redirect `source` to `destination` URL with optional 30x status `code`.
    +   *
    +   * Both `source` and `destination` can be route names.
    +   *
    +   * ```javascript
    +   * router.redirect('/login', 'sign-in');
    +   * ```
    +   *
    +   * This is equivalent to:
    +   *
    +   * ```javascript
    +   * router.all('/login', ctx => {
    +   *   ctx.redirect('/sign-in');
    +   *   ctx.status = 301;
    +   * });
    +   * ```
    +   *
    +   * @param source - URL or route name
    +   * @param destination - URL or route name
    +   * @param code - HTTP status code (default: 301)
    +   * @returns This router instance
    +   */
    +  redirect(
    +    source: string | symbol,
    +    destination: string | symbol,
    +    code?: number
    +  ): Router<StateT, ContextT> {
    +    let resolvedSource: string = source as string;
    +    let resolvedDestination: string = destination as string;
    +
    +    if (typeof source === 'symbol' || source[0] !== '/') {
    +      const sourceUrl = this.url(source as string);
    +      if (sourceUrl instanceof Error) throw sourceUrl;
    +      resolvedSource = sourceUrl;
    +    }
    +
    +    if (
    +      typeof destination === 'symbol' ||
    +      (destination[0] !== '/' && !destination.includes('://'))
    +    ) {
    +      const destinationUrl = this.url(destination as string);
    +      if (destinationUrl instanceof Error) throw destinationUrl;
    +      resolvedDestination = destinationUrl;
    +    }
    +
    +    const result = this.all(
    +      resolvedSource,
    +      (
    +        context: ParameterizedContext<
    +          StateT,
    +          ContextT & RouterParameterContext<StateT, ContextT>
    +        >
    +      ) => {
    +        context.redirect(resolvedDestination);
    +        context.status = code || 301;
    +      }
    +    );
    +    return result as Router<StateT, ContextT>;
    +  }
    +
    +  /**
    +   * Create and register a route.
    +   *
    +   * @param path - Path string
    +   * @param methods - Array of HTTP verbs
    +   * @param middleware - Middleware functions
    +   * @param additionalOptions - Additional options
    +   * @returns Created layer
    +   * @private
    +   */
    +  register(
    +    path: string | RegExp | string[],
    +    methods: string[],
    +    middleware:
    +      | RouterMiddleware<StateT, ContextT>
    +      | RouterMiddleware<StateT, ContextT>[],
    +    additionalOptions: LayerOptions = {}
    +  ): Layer | Router<StateT, ContextT> {
    +    const mergedOptions = { ...this.opts, ...additionalOptions };
    +
    +    if (Array.isArray(path)) {
    +      return this._registerMultiplePaths(
    +        path,
    +        methods,
    +        middleware as any,
    +        mergedOptions
    +      );
    +    }
    +
    +    const routeLayer = this._createRouteLayer(
    +      path,
    +      methods,
    +      middleware as any,
    +      mergedOptions
    +    );
    +
    +    if (this.opts.prefix) {
    +      routeLayer.setPrefix(this.opts.prefix);
    +    }
    +
    +    applyAllParameterMiddleware(routeLayer, this.params as any);
    +
    +    this.stack.push(routeLayer);
    +
    +    debug('defined route %s %s', routeLayer.methods, routeLayer.path);
    +
    +    return routeLayer;
    +  }
    +
    +  /**
    +   * Register multiple paths with the same configuration
    +   * @private
    +   */
    +  private _registerMultiplePaths(
    +    pathArray: string[],
    +    methods: string[],
    +    middleware:
    +      | RouterMiddleware<StateT, ContextT>
    +      | RouterMiddleware<StateT, ContextT>[],
    +    options: LayerOptions
    +  ): Router<StateT, ContextT> {
    +    for (const singlePath of pathArray) {
    +      this.register.call(this, singlePath, methods, middleware, options);
    +    }
    +
    +    return this;
    +  }
    +
    +  /**
    +   * Create a route layer with given configuration
    +   * @private
    +   */
    +  private _createRouteLayer(
    +    path: string | RegExp,
    +    methods: string[],
    +    middleware: RouterMiddleware | RouterMiddleware[],
    +    options: LayerOptions
    +  ): Layer {
    +    return new Layer(path, methods, middleware, {
    +      end: options.end === false ? options.end : true,
    +      name: options.name,
    +      sensitive: options.sensitive || false,
    +      strict: options.strict || false,
    +      prefix: options.prefix || '',
    +      ignoreCaptures: options.ignoreCaptures,
    +      pathAsRegExp: options.pathAsRegExp
    +    });
    +  }
    +
    +  /**
    +   * Lookup route with given `name`.
    +   *
    +   * @param name - Route name
    +   * @returns Matched layer or false
    +   */
    +  route(name: string): Layer | false {
    +    const matchingRoute = this.stack.find((route) => route.name === name);
    +    return matchingRoute || false;
    +  }
    +
    +  /**
    +   * Generate URL for route. Takes a route name and map of named `params`.
    +   *
    +   * @example
    +   *
    +   * ```javascript
    +   * router.get('user', '/users/:id', (ctx, next) => {
    +   *   // ...
    +   * });
    +   *
    +   * router.url('user', 3);
    +   * // => "/users/3"
    +   *
    +   * router.url('user', { id: 3 });
    +   * // => "/users/3"
    +   *
    +   * router.use((ctx, next) => {
    +   *   // redirect to named route
    +   *   ctx.redirect(ctx.router.url('sign-in'));
    +   * })
    +   *
    +   * router.url('user', { id: 3 }, { query: { limit: 1 } });
    +   * // => "/users/3?limit=1"
    +   *
    +   * router.url('user', { id: 3 }, { query: "limit=1" });
    +   * // => "/users/3?limit=1"
    +   * ```
    +   *
    +   * @param name - Route name
    +   * @param args - URL parameters
    +   * @returns Generated URL or Error
    +   */
    +  url(name: string, ...arguments_: any[]): string | Error {
    +    const route = this.route(name);
    +    if (route) return route.url.apply(route, arguments_);
    +
    +    return new Error(`No route found for name: ${String(name)}`);
    +  }
    +
    +  /**
    +   * Match given `path` and return corresponding routes.
    +   *
    +   * @param path - Request path
    +   * @param method - HTTP method
    +   * @returns Match result with matched layers
    +   * @private
    +   */
    +  match(path: string, method: string): MatchResult {
    +    const matchResult: MatchResult = {
    +      path: [],
    +      pathAndMethod: [],
    +      route: false
    +    };
    +
    +    for (const layer of this.stack) {
    +      debug('test %s %s', layer.path, layer.regexp);
    +
    +      // eslint-disable-next-line unicorn/prefer-regexp-test -- layer.match() is a method, not String.match()
    +      if (layer.match(path)) {
    +        matchResult.path.push(layer);
    +
    +        const isMiddleware = layer.methods.length === 0;
    +        const matchesMethod = layer.methods.includes(method);
    +
    +        if (isMiddleware || matchesMethod) {
    +          matchResult.pathAndMethod.push(layer);
    +
    +          if (layer.methods.length > 0) {
    +            matchResult.route = true;
    +          }
    +        }
    +      }
    +    }
    +
    +    return matchResult;
    +  }
    +
    +  /**
    +   * Match given `input` to allowed host
    +   * @param input - Host to check
    +   * @returns Whether host matches
    +   */
    +  matchHost(input?: string): boolean {
    +    const { host } = this;
    +
    +    if (!host) {
    +      return true;
    +    }
    +
    +    if (!input) {
    +      return false;
    +    }
    +
    +    if (typeof host === 'string') {
    +      return input === host;
    +    }
    +
    +    if (Array.isArray(host)) {
    +      return host.includes(input);
    +    }
    +
    +    if (host instanceof RegExp) {
    +      return host.test(input);
    +    }
    +
    +    return false;
    +  }
    +
    +  /**
    +   * Run middleware for named route parameters. Useful for auto-loading or
    +   * validation.
    +   *
    +   * @example
    +   *
    +   * ```javascript
    +   * router
    +   *   .param('user', (id, ctx, next) => {
    +   *     ctx.user = users[id];
    +   *     if (!ctx.user) return ctx.status = 404;
    +   *     return next();
    +   *   })
    +   *   .get('/users/:user', ctx => {
    +   *     ctx.body = ctx.user;
    +   *   })
    +   *   .get('/users/:user/friends', ctx => {
    +   *     return ctx.user.getFriends().then(function(friends) {
    +   *       ctx.body = friends;
    +   *     });
    +   *   })
    +   *   // /users/3 => {"id": 3, "name": "Alex"}
    +   *   // /users/3/friends => [{"id": 4, "name": "TJ"}]
    +   * ```
    +   *
    +   * @param param - Parameter name
    +   * @param middleware - Parameter middleware
    +   * @returns This router instance
    +   */
    +  param(
    +    parameter: string,
    +    middleware: RouterParameterMiddleware<StateT, ContextT>
    +  ): Router<StateT, ContextT> {
    +    if (!this.params[parameter]) {
    +      this.params[parameter] = [];
    +    }
    +
    +    if (!Array.isArray(this.params[parameter])) {
    +      this.params[parameter] = [
    +        this.params[parameter] as RouterParameterMiddleware<StateT, ContextT>
    +      ];
    +    }
    +
    +    (
    +      this.params[parameter] as RouterParameterMiddleware<StateT, ContextT>[]
    +    ).push(middleware);
    +
    +    for (const route of this.stack) {
    +      route.param(parameter, middleware as any);
    +    }
    +
    +    return this;
    +  }
    +
    +  /**
    +   * Helper method for registering HTTP verb routes
    +   * @internal - Used by dynamically added HTTP methods
    +   */
    +  _registerMethod(
    +    method: string,
    +    ...arguments_: any[]
    +  ): Router<StateT, ContextT> {
    +    let name: string | undefined;
    +    let path: string | RegExp | string[];
    +    let middleware: RouterMiddleware<any, any>[];
    +
    +    if (
    +      arguments_.length >= 2 &&
    +      (typeof arguments_[1] === 'string' || arguments_[1] instanceof RegExp)
    +    ) {
    +      name = arguments_[0];
    +      path = arguments_[1];
    +      middleware = arguments_.slice(2);
    +    } else {
    +      name = undefined;
    +      path = arguments_[0];
    +      middleware = arguments_.slice(1);
    +    }
    +
    +    if (
    +      typeof path !== 'string' &&
    +      !(path instanceof RegExp) &&
    +      (!Array.isArray(path) || path.length === 0)
    +    )
    +      throw new Error(
    +        `You have to provide a path when adding a ${method} handler`
    +      );
    +
    +    const options: LayerOptions = {
    +      name,
    +      pathAsRegExp: path instanceof RegExp
    +    };
    +
    +    this.register(path, [method], middleware as any, {
    +      ...this.opts,
    +      ...options
    +    });
    +    return this;
    +  }
    +
    +  /**
    +   * HTTP GET method
    +   */
    +  get<T = {}, U = {}, B = unknown>(
    +    name: string,
    +    path: string | RegExp,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  get<T = {}, U = {}, B = unknown>(
    +    path: string | RegExp | Array<string | RegExp>,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  get(...arguments_: any[]): Router<StateT, ContextT> {
    +    return this._registerMethod('get', ...arguments_);
    +  }
    +
    +  /**
    +   * HTTP POST method
    +   */
    +  post<T = {}, U = {}, B = unknown>(
    +    name: string,
    +    path: string | RegExp,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  post<T = {}, U = {}, B = unknown>(
    +    path: string | RegExp | Array<string | RegExp>,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  post(...arguments_: any[]): Router<StateT, ContextT> {
    +    return this._registerMethod('post', ...arguments_);
    +  }
    +
    +  /**
    +   * HTTP PUT method
    +   */
    +  put<T = {}, U = {}, B = unknown>(
    +    name: string,
    +    path: string | RegExp,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  put<T = {}, U = {}, B = unknown>(
    +    path: string | RegExp | Array<string | RegExp>,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  put(...arguments_: any[]): Router<StateT, ContextT> {
    +    return this._registerMethod('put', ...arguments_);
    +  }
    +
    +  /**
    +   * HTTP PATCH method
    +   */
    +  patch<T = {}, U = {}, B = unknown>(
    +    name: string,
    +    path: string | RegExp,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  patch<T = {}, U = {}, B = unknown>(
    +    path: string | RegExp | Array<string | RegExp>,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  patch(...arguments_: any[]): Router<StateT, ContextT> {
    +    return this._registerMethod('patch', ...arguments_);
    +  }
    +
    +  /**
    +   * HTTP DELETE method
    +   */
    +  delete<T = {}, U = {}, B = unknown>(
    +    name: string,
    +    path: string | RegExp,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  delete<T = {}, U = {}, B = unknown>(
    +    path: string | RegExp | Array<string | RegExp>,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  delete(...arguments_: any[]): Router<StateT, ContextT> {
    +    return this._registerMethod('delete', ...arguments_);
    +  }
    +
    +  /**
    +   * HTTP DELETE method alias (del)
    +   */
    +  del<T = {}, U = {}, B = unknown>(
    +    name: string,
    +    path: string | RegExp,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  del<T = {}, U = {}, B = unknown>(
    +    path: string | RegExp | Array<string | RegExp>,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  del(...arguments_: any[]): Router<StateT, ContextT> {
    +    return (this.delete as any).apply(this, arguments_);
    +  }
    +
    +  /**
    +   * HTTP HEAD method
    +   */
    +  head<T = {}, U = {}, B = unknown>(
    +    name: string,
    +    path: string | RegExp,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  head<T = {}, U = {}, B = unknown>(
    +    path: string | RegExp | Array<string | RegExp>,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  head(...arguments_: any[]): Router<StateT, ContextT> {
    +    return this._registerMethod('head', ...arguments_);
    +  }
    +
    +  /**
    +   * HTTP OPTIONS method
    +   */
    +  options<T = {}, U = {}, B = unknown>(
    +    name: string,
    +    path: string | RegExp,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  options<T = {}, U = {}, B = unknown>(
    +    path: string | RegExp | Array<string | RegExp>,
    +    ...middleware: Array<RouterMiddleware<StateT & T, ContextT & U, B>>
    +  ): Router<StateT, ContextT>;
    +  options(...arguments_: any[]): Router<StateT, ContextT> {
    +    return this._registerMethod('options', ...arguments_);
    +  }
    +}
    +
    +/**
    + * Create `router.verb()` methods, where *verb* is one of the HTTP verbs such
    + * as `router.get()` or `router.post()`.
    + *
    + * Match URL patterns to callback functions or controller actions using `router.verb()`,
    + * where **verb** is one of the HTTP verbs such as `router.get()` or `router.post()`.
    + *
    + * Additionally, `router.all()` can be used to match against all methods.
    + */
    +
    +for (const httpMethod of httpMethods) {
    +  const isAlreadyDefined =
    +    COMMON_HTTP_METHODS.includes(httpMethod) ||
    +    (Router.prototype as any)[httpMethod];
    +
    +  if (!isAlreadyDefined) {
    +    Object.defineProperty(Router.prototype, httpMethod, {
    +      value: function (this: Router, ...arguments_: any[]) {
    +        return this._registerMethod(httpMethod, ...arguments_);
    +      },
    +      writable: true,
    +      configurable: true,
    +      enumerable: false
    +    });
    +  }
    +}
    
  • src/types.ts+256 0 added
    @@ -0,0 +1,256 @@
    +/**
    + * Type definitions for @koa/router
    + */
    +
    +import type {
    +  Middleware,
    +  ParameterizedContext,
    +  DefaultContext,
    +  DefaultState
    +} from 'koa';
    +import type Router from './router';
    +import type Layer from './layer';
    +
    +export interface RouterOptions {
    +  /**
    +   * Only run last matched route's controller when there are multiple matches
    +   */
    +  exclusive?: boolean;
    +
    +  /**
    +   * Prefix for all routes
    +   */
    +  prefix?: string;
    +
    +  /**
    +   * Host for router match (string, array of strings, or RegExp)
    +   * - string: exact match
    +   * - string[]: matches if input equals any string in the array
    +   * - RegExp: pattern match
    +   */
    +  host?: string | string[] | RegExp;
    +
    +  /**
    +   * HTTP methods this router should respond to
    +   */
    +  methods?: string[];
    +
    +  /**
    +   * Path to use for routing (internal)
    +   */
    +  routerPath?: string;
    +
    +  /**
    +   * Whether to use case-sensitive routing
    +   */
    +  sensitive?: boolean;
    +
    +  /**
    +   * Whether trailing slashes are significant
    +   */
    +  strict?: boolean;
    +}
    +
    +export interface LayerOptions {
    +  /**
    +   * Route name for URL generation
    +   */
    +  name?: string | null;
    +
    +  /**
    +   * Case sensitive routing
    +   */
    +  sensitive?: boolean;
    +
    +  /**
    +   * Require trailing slash
    +   */
    +  strict?: boolean;
    +
    +  /**
    +   * Whether trailing slashes matter (path-to-regexp v8)
    +   */
    +  trailing?: boolean;
    +
    +  /**
    +   * Route path ends at this path
    +   */
    +  end?: boolean;
    +
    +  /**
    +   * Prefix for the route
    +   */
    +  prefix?: string;
    +
    +  /**
    +   * Ignore captures in route matching
    +   */
    +  ignoreCaptures?: boolean;
    +
    +  /**
    +   * Treat path as a regular expression
    +   */
    +  pathAsRegExp?: boolean;
    +}
    +
    +export interface UrlOptions {
    +  /**
    +   * Query string parameters
    +   */
    +  query?: Record<string, any> | string;
    +
    +  [key: string]: any;
    +}
    +
    +export interface RouterParameterContext<
    +  StateT = DefaultState,
    +  ContextT = DefaultContext
    +> {
    +  /**
    +   * URL parameters
    +   */
    +  params: Record<string, string>;
    +
    +  /**
    +   * Router instance
    +   */
    +  router: Router<StateT, ContextT>;
    +
    +  /**
    +   * Matched route path (internal)
    +   */
    +  _matchedRoute?: string | RegExp;
    +
    +  /**
    +   * Matched route name (internal)
    +   */
    +  _matchedRouteName?: string;
    +}
    +
    +export interface RouterParameterMiddleware<
    +  StateT = DefaultState,
    +  ContextT = DefaultContext,
    +  BodyT = unknown
    +> {
    +  (
    +    parameterValue: string,
    +    context: RouterContext<StateT, ContextT, BodyT>,
    +    next: () => Promise<any>
    +  ): any;
    +}
    +
    +export interface MatchResult {
    +  /**
    +   * Layers that matched the path
    +   */
    +  path: Layer[];
    +
    +  /**
    +   * Layers that matched both path and HTTP method
    +   */
    +  pathAndMethod: Layer[];
    +
    +  /**
    +   * Whether a route (not just middleware) was matched
    +   */
    +  route: boolean;
    +}
    +
    +export interface AllowedMethodsOptions {
    +  /**
    +   * Throw error instead of setting status and header
    +   */
    +  throw?: boolean;
    +
    +  /**
    +   * Throw the returned value in place of the default NotImplemented error
    +   */
    +  notImplemented?: () => Error;
    +
    +  /**
    +   * Throw the returned value in place of the default MethodNotAllowed error
    +   */
    +  methodNotAllowed?: () => Error;
    +}
    +
    +/**
    + * Extended Koa context with router-specific properties
    + * Matches the structure from @types/koa-router
    + */
    +export type RouterContext<
    +  StateT = DefaultState,
    +  ContextT = DefaultContext,
    +  BodyT = unknown
    +> = ParameterizedContext<
    +  StateT,
    +  ContextT & RouterParameterContext<StateT, ContextT>,
    +  BodyT
    +> & {
    +  /**
    +   * Request with params (params added dynamically)
    +   */
    +  request: {
    +    params?: Record<string, string>;
    +  };
    +
    +  /**
    +   * Path of matched route
    +   */
    +  routerPath?: string;
    +
    +  /**
    +   * Name of matched route
    +   */
    +  routerName?: string;
    +
    +  /**
    +   * Array of matched layers
    +   */
    +  matched?: Layer[];
    +
    +  /**
    +   * Captured values from path
    +   */
    +  captures?: string[];
    +
    +  /**
    +   * New router path (for nested routers)
    +   */
    +  newRouterPath?: string;
    +
    +  /**
    +   * Track param middleware execution (internal)
    +   */
    +  _matchedParams?: WeakMap<Function, boolean>;
    +};
    +
    +/**
    + * Router middleware function type
    + */
    +export type RouterMiddleware<
    +  StateT = DefaultState,
    +  ContextT = DefaultContext,
    +  BodyT = unknown
    +> = Middleware<
    +  StateT,
    +  ContextT & RouterParameterContext<StateT, ContextT>,
    +  BodyT
    +>;
    +
    +/**
    + * HTTP method names in lowercase
    + */
    +export type HttpMethod =
    +  | 'get'
    +  | 'post'
    +  | 'put'
    +  | 'patch'
    +  | 'delete'
    +  | 'del'
    +  | 'head'
    +  | 'options'
    +  | 'connect'
    +  | 'trace'
    +  | string;
    +
    +export { type default as Layer } from './layer';
    
  • src/utils/http-methods.ts+27 0 added
    @@ -0,0 +1,27 @@
    +/**
    + * HTTP methods utilities
    + */
    +
    +import http from 'node:http';
    +
    +/**
    + * Get all HTTP methods in lowercase
    + * @returns Array of HTTP method names in lowercase
    + */
    +export function getAllHttpMethods(): string[] {
    +  return http.METHODS.map((method) => method.toLowerCase());
    +}
    +
    +/**
    + * Common HTTP methods that are explicitly defined
    + */
    +export const COMMON_HTTP_METHODS: string[] = [
    +  'get',
    +  'post',
    +  'put',
    +  'patch',
    +  'delete',
    +  'del',
    +  'head',
    +  'options'
    +];
    
  • src/utils/parameter-helpers.ts+66 0 added
    @@ -0,0 +1,66 @@
    +/**
    + * Parameter handling utilities for router.param() functionality
    + */
    +
    +import type { RouterParameterMiddleware, Layer } from '../types';
    +import type Router from '../router';
    +
    +/**
    + * Normalize param middleware to always be an array
    + * @param paramMiddleware - Single middleware or array
    + * @returns Array of middleware functions
    + */
    +export function normalizeParameterMiddleware(
    +  parameterMiddleware:
    +    | RouterParameterMiddleware
    +    | RouterParameterMiddleware[]
    +    | undefined
    +): RouterParameterMiddleware[] {
    +  if (!parameterMiddleware) {
    +    return [];
    +  }
    +
    +  if (Array.isArray(parameterMiddleware)) {
    +    return parameterMiddleware;
    +  }
    +
    +  return [parameterMiddleware];
    +}
    +
    +/**
    + * Apply param middleware to a route
    + * @param route - Route layer to apply middleware to
    + * @param paramName - Name of the parameter
    + * @param paramMiddleware - Middleware to apply
    + */
    +export function applyParameterMiddlewareToRoute(
    +  route: Layer | Router,
    +  parameterName: string,
    +  parameterMiddleware: RouterParameterMiddleware | RouterParameterMiddleware[]
    +): void {
    +  const middlewareList = normalizeParameterMiddleware(parameterMiddleware);
    +
    +  for (const middleware of middlewareList) {
    +    route.param(parameterName, middleware);
    +  }
    +}
    +
    +/**
    + * Apply all param middleware from params object to a route
    + * @param route - Route layer
    + * @param paramsObject - Object mapping param names to middleware
    + */
    +export function applyAllParameterMiddleware(
    +  route: Layer,
    +  parametersObject: Record<
    +    string,
    +    RouterParameterMiddleware | RouterParameterMiddleware[]
    +  >
    +): void {
    +  const parameterNames = Object.keys(parametersObject);
    +
    +  for (const parameterName of parameterNames) {
    +    const parameterMiddleware = parametersObject[parameterName];
    +    applyParameterMiddlewareToRoute(route, parameterName, parameterMiddleware);
    +  }
    +}
    
  • src/utils/path-helpers.ts+75 0 added
    @@ -0,0 +1,75 @@
    +/**
    + * Path handling utilities
    + */
    +
    +import { compilePathToRegexp } from './path-to-regexp-wrapper';
    +import type { LayerOptions } from '../types';
    +
    +/**
    + * Check if a path has parameters (like :id, :name, etc.)
    + * @param path - Path to check
    + * @param options - path-to-regexp options
    + * @returns True if path contains parameters
    + */
    +export function hasPathParameters(
    +  path: string,
    +  options: LayerOptions = {}
    +): boolean {
    +  if (!path) {
    +    return false;
    +  }
    +
    +  const { keys } = compilePathToRegexp(path, options);
    +  return keys.length > 0;
    +}
    +
    +/**
    + * Determine the appropriate middleware path based on router configuration
    + * @param explicitPath - Explicitly provided path (if any)
    + * @param hasPrefixParameters - Whether the router prefix has parameters
    + * @returns Object with path and pathAsRegExp flag
    + */
    +export function determineMiddlewarePath(
    +  explicitPath: string | RegExp | undefined,
    +  hasPrefixParameters: boolean
    +): { path: string | RegExp; pathAsRegExp: boolean } {
    +  if (explicitPath !== undefined) {
    +    if (typeof explicitPath === 'string') {
    +      if (explicitPath === '') {
    +        return {
    +          path: '{/*rest}',
    +          pathAsRegExp: false
    +        };
    +      }
    +
    +      if (explicitPath === '/') {
    +        return {
    +          path: '/',
    +          pathAsRegExp: false
    +        };
    +      }
    +
    +      return {
    +        path: explicitPath,
    +        pathAsRegExp: false
    +      };
    +    }
    +
    +    return {
    +      path: explicitPath,
    +      pathAsRegExp: true
    +    };
    +  }
    +
    +  if (hasPrefixParameters) {
    +    return {
    +      path: '{/*rest}',
    +      pathAsRegExp: false
    +    };
    +  }
    +
    +  return {
    +    path: String.raw`(?:\/|$)`,
    +    pathAsRegExp: true
    +  };
    +}
    
  • src/utils/path-to-regexp-wrapper.ts+152 0 added
    @@ -0,0 +1,152 @@
    +/**
    + * Path-to-regexp wrapper utility
    + *
    + * Centralizes all path-to-regexp imports and provides a clean interface.
    + * This abstraction allows easier maintenance and potential future changes.
    + */
    +
    +import { pathToRegexp, compile, parse } from 'path-to-regexp';
    +import type { Key } from 'path-to-regexp';
    +import type { LayerOptions } from '../types';
    +
    +/**
    + * Options for path-to-regexp operations
    + * Based on path-to-regexp v8 options
    + */
    +export interface PathToRegexpOptions {
    +  /**
    +   * Case sensitive matching
    +   */
    +  sensitive?: boolean;
    +
    +  /**
    +   * Whether trailing slashes are significant
    +   * Note: path-to-regexp v8 renamed 'strict' to 'trailing'
    +   */
    +  strict?: boolean;
    +  trailing?: boolean;
    +
    +  /**
    +   * Route path ends at this path
    +   */
    +  end?: boolean;
    +
    +  /**
    +   * Prefix for the route
    +   */
    +  prefix?: string;
    +
    +  /**
    +   * Ignore captures in route matching
    +   */
    +  ignoreCaptures?: boolean;
    +
    +  /**
    +   * Treat path as a regular expression
    +   */
    +  pathAsRegExp?: boolean;
    +
    +  /**
    +   * Additional options passed to path-to-regexp
    +   */
    +  [key: string]: any;
    +}
    +
    +/**
    + * Result of path-to-regexp compilation
    + */
    +export interface PathToRegexpResult {
    +  regexp: RegExp;
    +  keys: Key[];
    +}
    +
    +/**
    + * Compile a path pattern to a regular expression
    + *
    + * @param path - Path pattern string
    + * @param options - Compilation options
    + * @returns Object with regexp and parameter keys
    + */
    +export function compilePathToRegexp(
    +  path: string,
    +  options: PathToRegexpOptions = {}
    +): PathToRegexpResult {
    +  const normalizedOptions: any = { ...options };
    +
    +  if ('strict' in normalizedOptions && !('trailing' in normalizedOptions)) {
    +    normalizedOptions.trailing = normalizedOptions.strict !== true;
    +    delete normalizedOptions.strict;
    +  }
    +
    +  delete normalizedOptions.pathAsRegExp;
    +  delete normalizedOptions.ignoreCaptures;
    +  delete normalizedOptions.prefix;
    +
    +  const { regexp, keys } = pathToRegexp(path, normalizedOptions);
    +  return { regexp, keys };
    +}
    +
    +/**
    + * Compile a path pattern to a URL generator function
    + *
    + * @param path - Path pattern string
    + * @param options - Compilation options
    + * @returns Function that generates URLs from parameters
    + */
    +export function compilePath(
    +  path: string,
    +  options: Record<string, any> = {}
    +): (parameters?: Record<string, string>) => string {
    +  return compile(path, options);
    +}
    +
    +/**
    + * Parse a path pattern into tokens
    + *
    + * @param path - Path pattern string
    + * @param options - Parse options
    + * @returns Array of tokens
    + */
    +export function parsePath(
    +  path: string,
    +  options?: Record<string, any>
    +): ReturnType<typeof parse> {
    +  return parse(path, options);
    +}
    +
    +/**
    + * Re-export Key type for convenience
    + */
    +
    +/**
    + * Normalize LayerOptions to path-to-regexp options
    + * Handles the strict/trailing option conversion
    + *
    + * @param options - Layer options
    + * @returns Normalized path-to-regexp options
    + */
    +export function normalizeLayerOptionsToPathToRegexp(
    +  options: LayerOptions = {}
    +): Record<string, any> {
    +  const normalized: Record<string, any> = {
    +    sensitive: options.sensitive,
    +    end: options.end,
    +    strict: options.strict,
    +    trailing: options.trailing
    +  };
    +
    +  if ('strict' in normalized && !('trailing' in normalized)) {
    +    normalized.trailing = normalized.strict !== true;
    +    delete normalized.strict;
    +  }
    +
    +  for (const key of Object.keys(normalized)) {
    +    if (normalized[key] === undefined) {
    +      delete normalized[key];
    +    }
    +  }
    +
    +  return normalized;
    +}
    +
    +export { type Key } from 'path-to-regexp';
    
  • test/index.test.ts+3 2 renamed
    @@ -1,11 +1,12 @@
     /**
      * Module tests
      */
    -const assert = require('node:assert');
    +import { describe, it } from 'node:test';
    +import assert from 'node:assert';
    +import Router from '../src';
     
     describe('module', () => {
       it('should expose Router', () => {
    -    const Router = require('..');
         assert.strictEqual(Boolean(Router), true);
         assert.strictEqual(typeof Router, 'function');
       });
    
  • test/layer.test.ts+104 27 renamed
    @@ -1,14 +1,15 @@
     /**
      * Route tests
      */
    -const http = require('node:http');
    -const assert = require('node:assert');
    +import { describe, it, before } from 'node:test';
    +import assert from 'node:assert';
    +import http from 'node:http';
     
    -const Koa = require('koa');
    -const request = require('supertest');
    +import Koa from 'koa';
    +import request from 'supertest';
     
    -const Router = require('../../lib/router');
    -const Layer = require('../../lib/layer');
    +import Router from '../src';
    +import Layer from '../src/layer';
     
     describe('Layer', () => {
       it('composes multiple callbacks/middleware', async () => {
    @@ -17,11 +18,11 @@ describe('Layer', () => {
         app.use(router.routes());
         router.get(
           '/:category/:title',
    -      (ctx, next) => {
    +      (ctx: any, next: any) => {
             ctx.status = 500;
             return next();
           },
    -      (ctx, next) => {
    +      (ctx: any, next: any) => {
             ctx.status = 204;
             return next();
           }
    @@ -37,7 +38,7 @@ describe('Layer', () => {
           const app = new Koa();
           const router = new Router();
           app.use(router.routes());
    -      router.get('/:category/:title', (ctx) => {
    +      router.get('/:category/:title', (ctx: any) => {
             assert.strictEqual(typeof ctx.params, 'object');
             assert.strictEqual(ctx.params.category, 'match');
             assert.strictEqual(ctx.params.title, 'this');
    @@ -52,7 +53,7 @@ describe('Layer', () => {
           const app = new Koa();
           const router = new Router();
           app.use(router.routes());
    -      router.get('/:category/:title', (ctx) => {
    +      router.get('/:category/:title', (ctx: any) => {
             assert.strictEqual(typeof ctx.params, 'object');
             assert.strictEqual(ctx.params.category, '100%');
             assert.strictEqual(ctx.params.title, '101%');
    @@ -63,18 +64,33 @@ describe('Layer', () => {
             .expect(204);
         });
     
    +    it('preserves plus signs in URL path parameters', async () => {
    +      const app = new Koa();
    +      const router = new Router();
    +      app.use(router.routes());
    +      router.get('/users/:username', (ctx: any) => {
    +        assert.strictEqual(ctx.params.username, 'john+doe');
    +        ctx.status = 200;
    +        ctx.body = { username: ctx.params.username };
    +      });
    +      const res = await request(http.createServer(app.callback()))
    +        .get('/users/john%2Bdoe')
    +        .expect(200);
    +      assert.strictEqual(res.body.username, 'john+doe');
    +    });
    +
         it('populates ctx.captures with regexp captures', async () => {
           const app = new Koa();
           const router = new Router();
           app.use(router.routes());
           router.get(
             /^\/api\/([^/]+)\/?/i,
    -        (ctx, next) => {
    +        (ctx: any, next: any) => {
               assert.strictEqual(Array.isArray(ctx.captures), true);
               assert.strictEqual(ctx.captures[0], '1');
               return next();
             },
    -        (ctx) => {
    +        (ctx: any) => {
               assert.strictEqual(Array.isArray(ctx.captures), true);
               assert.strictEqual(ctx.captures[0], '1');
               ctx.status = 204;
    @@ -91,12 +107,12 @@ describe('Layer', () => {
           app.use(router.routes());
           router.get(
             /^\/api\/([^/]+)\/?/i,
    -        (ctx, next) => {
    +        (ctx: any, next: any) => {
               assert.strictEqual(typeof ctx.captures, 'object');
               assert.strictEqual(ctx.captures[0], '101%');
               return next();
             },
    -        (ctx) => {
    +        (ctx: any) => {
               assert.strictEqual(typeof ctx.captures, 'object');
               assert.strictEqual(ctx.captures[0], '101%');
               ctx.status = 204;
    @@ -113,12 +129,12 @@ describe('Layer', () => {
           app.use(router.routes());
           router.get(
             /^\/api(\/.+)?/i,
    -        (ctx, next) => {
    +        (ctx: any, next: any) => {
               assert.strictEqual(typeof ctx.captures, 'object');
               assert.strictEqual(ctx.captures[0], undefined);
               return next();
             },
    -        (ctx) => {
    +        (ctx: any) => {
               assert.strictEqual(typeof ctx.captures, 'object');
               assert.strictEqual(ctx.captures[0], undefined);
               ctx.status = 204;
    @@ -133,21 +149,21 @@ describe('Layer', () => {
           app.use(router.routes());
           const notexistHandle = undefined;
           assert.throws(
    -        () => router.get('/foo', notexistHandle),
    +        () => router.get('/foo', notexistHandle as any),
             new Error(
               'get `/foo`: `middleware` must be a function, not `undefined`'
             )
           );
     
           assert.throws(
    -        () => router.get('foo router', '/foo', notexistHandle),
    +        () => router.get('foo router', '/foo', notexistHandle as any),
             new Error(
               'get `foo router`: `middleware` must be a function, not `undefined`'
             )
           );
     
           assert.throws(
    -        () => router.post('/foo', () => {}, notexistHandle),
    +        () => router.post('/foo', () => {}, notexistHandle as any),
             new Error(
               'post `/foo`: `middleware` must be a function, not `undefined`'
             )
    @@ -163,12 +179,12 @@ describe('Layer', () => {
             '/users/:user',
             ['GET'],
             [
    -          (ctx) => {
    +          (ctx: any) => {
                 ctx.body = ctx.user;
               }
             ]
           );
    -      route.param('user', (id, ctx, next) => {
    +      route.param('user', (id, ctx: any, next) => {
             ctx.user = { name: 'alex' };
             if (!id) {
               ctx.status = 404;
    @@ -192,12 +208,12 @@ describe('Layer', () => {
             '/users/:user',
             ['GET'],
             [
    -          (ctx) => {
    +          (ctx: any) => {
                 ctx.body = ctx.user;
               }
             ]
           );
    -      route.param('user', (id, ctx, next) => {
    +      route.param('user', (id, ctx: any, next) => {
             ctx.user = { name: 'alex' };
             if (!id) {
               ctx.status = 404;
    @@ -206,7 +222,7 @@ describe('Layer', () => {
     
             return next();
           });
    -      route.param('title', (id, ctx, next) => {
    +      route.param('title', (id, ctx: any, next) => {
             ctx.user = { name: 'mark' };
             if (!id) {
               ctx.status = 404;
    @@ -226,7 +242,7 @@ describe('Layer', () => {
       });
     
       describe('Layer#params()', () => {
    -    let route;
    +    let route: Layer;
     
         before(() => {
           route = new Layer('/:category', ['GET'], [() => {}]);
    @@ -250,6 +266,13 @@ describe('Layer', () => {
           assert.deepStrictEqual(params, { category: 'how to node' });
         });
     
    +    it('should preserve plus signs in path parameters (not convert to spaces)', () => {
    +      const route = new Layer('/users/:username', ['GET'], [() => {}]);
    +      const params = route.params('', ['john%2Bdoe']);
    +
    +      assert.deepStrictEqual(params, { username: 'john+doe' });
    +    });
    +
         it('should return an object with the same params if an error occurs', () => {
           const params = route.params('', ['%E0%A4%A']);
     
    @@ -313,12 +336,66 @@ describe('Layer', () => {
         });
     
         it('setPrefix method fails check Layer for path', () => {
    -      const route = new Layer(false, ['get'], [() => {}], {
    +      const route = new Layer(false as any, ['get'], [() => {}], {
             name: 'books'
           });
    -      route.path = false;
    +      route.path = false as any;
           const prefix = route.setPrefix('/TEST');
           assert.strictEqual(prefix.path, false);
         });
       });
    +
    +  describe('Layer#_reconfigurePathMatching()', () => {
    +    it('should use path-to-regexp when prefix has parameters and pathAsRegExp is true', async () => {
    +      const app = new Koa();
    +      const router = new Router();
    +      app.use(router.routes());
    +
    +      const route = new Layer(
    +        '/users/:id',
    +        ['GET'],
    +        [
    +          (ctx: any) => {
    +            ctx.body = { userId: ctx.params.id };
    +          }
    +        ],
    +        {
    +          pathAsRegExp: true
    +        }
    +      );
    +
    +      route.setPrefix('/api/:version');
    +
    +      router.stack.push(route);
    +
    +      const res = await request(http.createServer(app.callback()))
    +        .get('/api/v1/users/123')
    +        .expect(200);
    +
    +      assert.strictEqual(res.body.userId, '123');
    +      assert.strictEqual(route.opts.pathAsRegExp, false);
    +    });
    +
    +    it('should handle RegExp path when pathAsRegExp is true and prefix has no parameters', () => {
    +      const route = new Layer('/api/users/\\d+', ['GET'], [() => {}], {
    +        pathAsRegExp: true
    +      });
    +
    +      route.setPrefix('/v1');
    +
    +      assert.strictEqual(route.regexp instanceof RegExp, true);
    +    });
    +  });
    +
    +  describe('Layer#captures()', () => {
    +    it('should return empty array when regexp does not match', () => {
    +      const route = new Layer('/api/users/:id', ['GET'], [() => {}]);
    +
    +      route.regexp = /^\/api\/users\/\d+$/;
    +
    +      const captures = route.captures('/api/users/abc');
    +
    +      assert.deepStrictEqual(captures, []);
    +    });
    +  });
     });
    
  • test/router.test.ts+1310 75 renamed
    @@ -1,17 +1,18 @@
     /**
      * Router tests
      */
    -const fs = require('node:fs');
    -const http = require('node:http');
    -const path = require('node:path');
    -const assert = require('node:assert');
    +import { describe, it, before, after, beforeEach } from 'node:test';
    +import assert from 'node:assert';
    +import fs from 'node:fs';
    +import http from 'node:http';
    +import path from 'node:path';
     
    -const Koa = require('koa');
    -const methods = require('methods');
    -const request = require('supertest');
    +import Koa from 'koa';
    +import methods from 'methods';
    +import request from 'supertest';
     
    -const Router = require('../../lib/router');
    -const Layer = require('../../lib/layer');
    +import Router from '../src';
    +import Layer from '../src/layer';
     
     describe('Router', () => {
       it('creates new router with koa app', () => {
    @@ -268,14 +269,10 @@ describe('Router', () => {
         const router = new Router({ exclusive: true });
     
         router
    -      .get(
    -        'users_single',
    -        new RegExp('/users/:id(.*)'),  
    -        (ctx, next) => {
    -          ctx.body = { single: true };
    -          next();
    -        }
    -      )
    +      .get('users_single', new RegExp('/users/:id(.*)'), (ctx, next) => {
    +        ctx.body = { single: true };
    +        next();
    +      })
           .get('users_all', '/users/all', (ctx, next) => {
             ctx.body = { ...ctx.body, all: true };
             next();
    @@ -298,9 +295,7 @@ describe('Router', () => {
         router.get(
           'user_page',
           '/user/{*any}.jsx',
    -      () => {
    -        // no next()
    -      },
    +      () => {},
           (ctx) => {
             ctx.body = { order: 1 };
           }
    @@ -532,7 +527,7 @@ it('supports promises for route middleware', async () => {
       app.use(router.routes());
       const readVersion = () => {
         return new Promise((resolve, reject) => {
    -      const packagePath = path.join(__dirname, '..', '..', 'package.json');
    +      const packagePath = path.join(__dirname, '..', 'package.json');
           fs.readFile(packagePath, 'utf8', (err, data) => {
             if (err) return reject(err);
             resolve(JSON.parse(data).version);
    @@ -590,11 +585,9 @@ it('responds with 405 Method Not Allowed using the "throw" option', async () =>
       app.use(router.routes());
       app.use((ctx, next) => {
         return next().catch((err) => {
    -      // assert that the correct HTTPError was thrown
           assert.strictEqual(err.name, 'MethodNotAllowedError');
           assert.strictEqual(err.statusCode, 405);
     
    -      // translate the HTTPError to a normal response
           ctx.body = err.name;
           ctx.status = err.statusCode;
         });
    @@ -606,7 +599,6 @@ it('responds with 405 Method Not Allowed using the "throw" option', async () =>
       const res = await request(http.createServer(app.callback()))
         .post('/users')
         .expect(405);
    -  // the 'Allow' header is not set when throwing
       assert.strictEqual('allow' in res.header, false);
     });
     
    @@ -616,11 +608,9 @@ it('responds with user-provided throwable using the "throw" and "methodNotAllowe
       app.use(router.routes());
       app.use((ctx, next) => {
         return next().catch((err) => {
    -      // assert that the correct HTTPError was thrown
           assert.strictEqual(err.message, 'Custom Not Allowed Error');
           assert.strictEqual(err.statusCode, 405);
     
    -      // translate the HTTPError to a normal response
           ctx.body = err.body;
           ctx.status = err.statusCode;
         });
    @@ -647,7 +637,6 @@ it('responds with user-provided throwable using the "throw" and "methodNotAllowe
       const res = await request(http.createServer(app.callback()))
         .post('/users')
         .expect(405);
    -  // the 'Allow' header is not set when throwing
       assert.strictEqual('allow' in res.header, false);
       assert.deepStrictEqual(res.body, {
         error: 'Custom Not Allowed Error',
    @@ -672,11 +661,9 @@ it('responds with 501 Not Implemented using the "throw" option', async () => {
       app.use(router.routes());
       app.use((ctx, next) => {
         return next().catch((err) => {
    -      // assert that the correct HTTPError was thrown
           assert.strictEqual(err.name, 'NotImplementedError');
           assert.strictEqual(err.statusCode, 501);
     
    -      // translate the HTTPError to a normal response
           ctx.body = err.name;
           ctx.status = err.statusCode;
         });
    @@ -687,7 +674,6 @@ it('responds with 501 Not Implemented using the "throw" option', async () => {
       const res = await request(http.createServer(app.callback()))
         .search('/users')
         .expect(501);
    -  // the 'Allow' header is not set when throwing
       assert.strictEqual('allow' in res.header, false);
     });
     
    @@ -697,12 +683,10 @@ it('responds with user-provided throwable using the "throw" and "notImplemented"
       app.use(router.routes());
       app.use((ctx, next) => {
         return next().catch((err) => {
    -      // assert that our custom error was thrown
           assert.strictEqual(err.message, 'Custom Not Implemented Error');
           assert.strictEqual(err.type, 'custom');
           assert.strictEqual(err.statusCode, 501);
     
    -      // translate the HTTPError to a normal response
           ctx.body = err.body;
           ctx.status = err.statusCode;
         });
    @@ -728,7 +712,6 @@ it('responds with user-provided throwable using the "throw" and "notImplemented"
       const res = await request(http.createServer(app.callback()))
         .search('/users')
         .expect(501);
    -  // the 'Allow' header is not set when throwing
       assert.strictEqual('allow' in res.header, false);
       assert.deepStrictEqual(res.body, {
         error: 'Custom Not Implemented Error',
    @@ -749,7 +732,6 @@ it('does not send 405 if route matched but status is 404', async () => {
     });
     
     it('sets the allowed methods to a single Allow header #273', async () => {
    -  // https://tools.ietf.org/html/rfc7231#section-7.4.1
       const app = new Koa();
       const router = new Router();
       app.use(router.routes());
    @@ -781,7 +763,6 @@ it('supports custom routing detect path: ctx.routerPath', async () => {
       const app = new Koa();
       const router = new Router();
       app.use((ctx, next) => {
    -    // bind helloworld.example.com/users => example.com/helloworld/users
         const appname = ctx.request.hostname.split('.', 1)[0];
         ctx.newRouterPath = '/' + appname + ctx.path;
         return next();
    @@ -994,6 +975,82 @@ describe('Router#[verb]()', () => {
           );
         }
       });
    +
    +  it('validates parameters in route handlers (v14 approach for custom regex)', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    const uuidRegex =
    +      /^[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}$/;
    +
    +    router.get('/user/:id', (ctx) => {
    +      if (!uuidRegex.test(ctx.params.id)) {
    +        ctx.status = 400;
    +        ctx.body = { error: 'Invalid UUID format' };
    +        return;
    +      }
    +      ctx.body = { id: ctx.params.id, valid: true };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/user/123e4567-e89b-12d3-a456-426614174000')
    +      .expect(200);
    +    assert.strictEqual(res1.body.valid, true);
    +    assert.strictEqual(res1.body.id, '123e4567-e89b-12d3-a456-426614174000');
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/user/invalid-uuid')
    +      .expect(400);
    +    assert.strictEqual(res2.body.error, 'Invalid UUID format');
    +  });
    +
    +  it('validates parameters with middleware (v14 approach for custom regex)', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    function validateUUID(paramName) {
    +      const uuidRegex =
    +        /^[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}$/;
    +      return async (ctx, next) => {
    +        if (!uuidRegex.test(ctx.params[paramName])) {
    +          ctx.status = 400;
    +          ctx.body = { error: `Invalid ${paramName} format` };
    +          return;
    +        }
    +        await next();
    +      };
    +    }
    +
    +    router.get('/role/:id', validateUUID('id'), (ctx) => {
    +      ctx.body = { id: ctx.params.id, valid: true };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/role/550e8400-e29b-41d4-a716-446655440000')
    +      .expect(200);
    +    assert.strictEqual(res1.body.valid, true);
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/role/not-a-uuid')
    +      .expect(400);
    +    assert.strictEqual(res2.body.error, 'Invalid id format');
    +  });
    +
    +  it('should support del() method as alias for delete()', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +    app.use(router.routes());
    +    router.del('/users/:id', (ctx) => {
    +      ctx.body = { deleted: ctx.params.id };
    +    });
    +    await request(http.createServer(app.callback()))
    +      .delete('/users/123')
    +      .expect(200, { deleted: '123' });
    +  });
     });
     
     describe('Router#use()', () => {
    @@ -1112,6 +1169,142 @@ describe('Router#use()', () => {
     
         assert.strictEqual(secondRes.body.foobar, 'foobar');
       });
    +
    +  it('uses router middleware with RegExp path', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.use(/^\/api\//, (ctx, next) => {
    +      ctx.apiFlag = 'matched';
    +      return next();
    +    });
    +
    +    router.get('/api/users', (ctx) => {
    +      ctx.body = {
    +        flag: ctx.apiFlag || 'none'
    +      };
    +    });
    +
    +    router.get('/public/users', (ctx) => {
    +      ctx.body = {
    +        flag: ctx.apiFlag || 'none'
    +      };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/api/users')
    +      .expect(200);
    +    assert.strictEqual(res1.body.flag, 'matched');
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/public/users')
    +      .expect(200);
    +    assert.strictEqual(res2.body.flag, 'none');
    +  });
    +
    +  it('uses router middleware with RegExp matching multiple routes', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.use(/^\/a/, (ctx, next) => {
    +      ctx.regexpMatched = true;
    +      return next();
    +    });
    +
    +    router.post('/a1', (ctx) => {
    +      ctx.body = { route: 'a1', matched: ctx.regexpMatched || false };
    +    });
    +
    +    router.post('/a2', (ctx) => {
    +      ctx.body = { route: 'a2', matched: ctx.regexpMatched || false };
    +    });
    +
    +    router.post('/b1', (ctx) => {
    +      ctx.body = { route: 'b1', matched: ctx.regexpMatched || false };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .post('/a1')
    +      .expect(200);
    +    assert.strictEqual(res1.body.matched, true);
    +    assert.strictEqual(res1.body.route, 'a1');
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .post('/a2')
    +      .expect(200);
    +    assert.strictEqual(res2.body.matched, true);
    +    assert.strictEqual(res2.body.route, 'a2');
    +
    +    const res3 = await request(http.createServer(app.callback()))
    +      .post('/b1')
    +      .expect(200);
    +    assert.strictEqual(res3.body.matched, false);
    +    assert.strictEqual(res3.body.route, 'b1');
    +  });
    +
    +  it('uses router middleware with RegExp and multiple middleware functions', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.use(
    +      /^\/admin/,
    +      (ctx, next) => {
    +        ctx.step1 = 'done';
    +        return next();
    +      },
    +      (ctx, next) => {
    +        ctx.step2 = 'done';
    +        return next();
    +      }
    +    );
    +
    +    router.get('/admin/dashboard', (ctx) => {
    +      ctx.body = {
    +        step1: ctx.step1 || 'none',
    +        step2: ctx.step2 || 'none'
    +      };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .get('/admin/dashboard')
    +      .expect(200);
    +
    +    assert.strictEqual(res.body.step1, 'done');
    +    assert.strictEqual(res.body.step2, 'done');
    +  });
    +
    +  it('uses nested router with RegExp path', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +    const subrouter = new Router();
    +
    +    subrouter.get('/dashboard', (ctx) => {
    +      ctx.body = {
    +        auth: ctx.isAuthenticated || false
    +      };
    +    });
    +
    +    router.use(/^\/admin/, (ctx, next) => {
    +      ctx.isAuthenticated = true;
    +      return next();
    +    });
    +
    +    router.use('/admin', subrouter.routes());
    +
    +    app.use(router.routes());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .get('/admin/dashboard')
    +      .expect(200);
    +
    +    assert.strictEqual(res.body.auth, true);
    +  });
     });
     
     it('without path, does not set params.0 to the matched path - gh-247', async () => {
    @@ -1201,6 +1394,132 @@ it('assigns middleware to array of paths with function middleware and router nee
       });
     });
     
    +it('middleware with "/" in path array does not match all routes (gh-46)', async () => {
    +  const app = new Koa();
    +  const router = new Router({ prefix: '/account' });
    +  let middlewareCalled = false;
    +
    +  router.use(['/signout', '/', '/update'], async (ctx, next) => {
    +    middlewareCalled = true;
    +    await next();
    +  });
    +
    +  router.post('/signin', (ctx) => {
    +    ctx.body = 'signin';
    +  });
    +
    +  router.delete('/signout', (ctx) => {
    +    ctx.body = 'signout';
    +  });
    +
    +  router.get('/', (ctx) => {
    +    ctx.body = 'profile';
    +  });
    +
    +  app.use(router.routes());
    +
    +  middlewareCalled = false;
    +  await request(http.createServer(app.callback()))
    +    .delete('/account/signout')
    +    .expect(200);
    +  assert.strictEqual(
    +    middlewareCalled,
    +    true,
    +    'Middleware should run on /account/signout'
    +  );
    +
    +  middlewareCalled = false;
    +  await request(http.createServer(app.callback())).get('/account/').expect(200);
    +  assert.strictEqual(
    +    middlewareCalled,
    +    true,
    +    'Middleware should run on /account/'
    +  );
    +
    +  middlewareCalled = false;
    +  await request(http.createServer(app.callback()))
    +    .post('/account/signin')
    +    .expect(200);
    +  assert.strictEqual(
    +    middlewareCalled,
    +    false,
    +    'Middleware should NOT run on /account/signin'
    +  );
    +});
    +
    +it('nested router middleware does not affect unrelated routes (gh-90)', async () => {
    +  const app = new Koa();
    +  let checkAuthCalled = false;
    +
    +  const ajaxRouter = new Router({ prefix: '/a' });
    +  ajaxRouter.use(async (ctx, next) => {
    +    checkAuthCalled = true;
    +    await next();
    +  });
    +  ajaxRouter.get('/1', (ctx) => {
    +    ctx.body = 'route-1';
    +  });
    +
    +  const indexRouter = new Router();
    +  indexRouter.get('/', (ctx) => {
    +    ctx.body = 'index';
    +  });
    +  indexRouter.get('/about', (ctx) => {
    +    ctx.body = 'about';
    +  });
    +
    +  const mainRouter = new Router();
    +  mainRouter.use(ajaxRouter.routes());
    +  mainRouter.use(indexRouter.routes());
    +
    +  app.use(mainRouter.routes());
    +
    +  checkAuthCalled = false;
    +  await request(http.createServer(app.callback())).get('/a/1').expect(200);
    +  assert.strictEqual(checkAuthCalled, true, 'Middleware should run on /a/1');
    +
    +  checkAuthCalled = false;
    +  await request(http.createServer(app.callback())).get('/about').expect(200);
    +  assert.strictEqual(
    +    checkAuthCalled,
    +    false,
    +    'Middleware should NOT run on /about'
    +  );
    +});
    +
    +it('nested router middleware has access to parent path parameters', async () => {
    +  const app = new Koa();
    +  let capturedUserId = null;
    +
    +  const usersRouter = new Router();
    +  usersRouter.use(async (ctx, next) => {
    +    capturedUserId = ctx.params.userId;
    +    await next();
    +  });
    +  usersRouter.get('/', async (ctx) => {
    +    ctx.body = {
    +      id: ctx.params.userId,
    +      name: 'John Doe'
    +    };
    +  });
    +
    +  const mainRouter = new Router();
    +  mainRouter.use('/users/:userId', usersRouter.routes());
    +
    +  app.use(mainRouter.routes());
    +
    +  const res = await request(http.createServer(app.callback()))
    +    .get('/users/123')
    +    .expect(200);
    +
    +  assert.strictEqual(
    +    capturedUserId,
    +    '123',
    +    'Nested router middleware should have access to parent params'
    +  );
    +  assert.strictEqual(res.body.id, '123');
    +});
    +
     it('uses a same router middleware at given paths continuously - ZijianHe/koa-router#gh-244 gh-18', async () => {
       const app = new Koa();
       const base = new Router({ prefix: '/api' });
    @@ -1589,7 +1908,6 @@ describe('Router#param()', () => {
         const app = new Koa();
         const router = new Router();
         router
    -      // intentional random order
           .param('a', (id, ctx, next) => {
             ctx.state.loaded = [id];
             return next();
    @@ -1651,60 +1969,260 @@ describe('Router#param()', () => {
           cid: '2'
         });
       });
    -});
     
    -describe('Router#opts', () => {
    -  it('responds with 200', async () => {
    +  it('supports multiple param handlers for the same parameter', async () => {
         const app = new Koa();
    -    const router = new Router({
    -      strict: true
    -    });
    -    router.get('/info', (ctx) => {
    -      ctx.body = 'hello';
    -    });
    -    const res = await request(
    -      http.createServer(app.use(router.routes()).callback())
    -    )
    -      .get('/info')
    +    const router = new Router();
    +    const calls = [];
    +
    +    router
    +      .param('id', (id, ctx, next) => {
    +        calls.push('param1');
    +        ctx.state.param1 = true;
    +        return next();
    +      })
    +      .param('id', (id, ctx, next) => {
    +        calls.push('param2');
    +        ctx.state.param2 = true;
    +        return next();
    +      })
    +      .param('id', (id, ctx, next) => {
    +        calls.push('param3');
    +        ctx.state.param3 = true;
    +        return next();
    +      })
    +      .get('/:id', (ctx) => {
    +        calls.push('get');
    +        ctx.body = {
    +          param1: ctx.state.param1,
    +          param2: ctx.state.param2,
    +          param3: ctx.state.param3
    +        };
    +      });
    +
    +    app.use(router.routes());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .get('/test')
           .expect(200);
    -    assert.strictEqual(res.text, 'hello');
    +
    +    assert.deepStrictEqual(calls, ['param1', 'param2', 'param3', 'get']);
    +    assert.strictEqual(res.body.param1, true);
    +    assert.strictEqual(res.body.param2, true);
    +    assert.strictEqual(res.body.param3, true);
       });
     
    -  it('should allow setting a prefix', async () => {
    +  it('does not call param handlers multiple times with multiple matching routes', async () => {
         const app = new Koa();
    -    const routes = new Router({ prefix: '/things/:thing_id' });
    +    const router = new Router();
    +    const calls = [];
     
    -    routes.get('/list', (ctx) => {
    -      ctx.body = ctx.params;
    -    });
    +    router
    +      .param('id', (id, ctx, next) => {
    +        calls.push('param1');
    +        return next();
    +      })
    +      .get('/:id', (ctx, next) => {
    +        calls.push('get1');
    +        return next();
    +      })
    +      .param('id', (id, ctx, next) => {
    +        calls.push('param2');
    +        return next();
    +      })
    +      .get('/:id', (ctx) => {
    +        calls.push('get2');
    +        ctx.body = { calls: [...calls] };
    +      });
     
    -    app.use(routes.routes());
    +    app.use(router.routes());
     
         const res = await request(http.createServer(app.callback()))
    -      .get('/things/1/list')
    +      .get('/test')
           .expect(200);
    -    assert.strictEqual(res.body.thing_id, '1');
    -  });
     
    -  it('responds with 404 when has a trailing slash', async () => {
    -    const app = new Koa();
    -    const router = new Router({
    -      strict: true
    -    });
    -    router.get('/info', (ctx) => {
    -      ctx.body = 'hello';
    -    });
    -    await request(http.createServer(app.use(router.routes()).callback()))
    -      .get('/info/')
    -      .expect(404);
    +    assert.deepStrictEqual(calls, ['param1', 'param2', 'get1', 'get2']);
    +    assert.strictEqual(
    +      calls.filter((c) => c === 'param1').length,
    +      1,
    +      'param1 should only be called once'
    +    );
    +    assert.strictEqual(
    +      calls.filter((c) => c === 'param2').length,
    +      1,
    +      'param2 should only be called once'
    +    );
       });
    -});
     
    -describe('use middleware with opts', () => {
    -  it('responds with 200', async () => {
    +  it('does not call param handler multiple times with use middleware', async () => {
         const app = new Koa();
    -    const router = new Router({
    -      strict: true
    +    const router = new Router();
    +    const calls = [];
    +
    +    router
    +      .use('/:id', (ctx, next) => {
    +        calls.push('use1');
    +        return next();
    +      })
    +      .param('id', (id, ctx, next) => {
    +        calls.push('param1');
    +        return next();
    +      })
    +      .get('/:id', (ctx) => {
    +        calls.push('get1');
    +        ctx.body = { calls: [...calls] };
    +      });
    +
    +    app.use(router.routes());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .get('/test')
    +      .expect(200);
    +
    +    assert.strictEqual(
    +      calls.filter((c) => c === 'param1').length,
    +      1,
    +      'param1 should only be called once'
    +    );
    +    assert.strictEqual(
    +      calls.filter((c) => c === 'use1').length,
    +      1,
    +      'use1 should only be called once'
    +    );
    +    assert.strictEqual(
    +      calls.filter((c) => c === 'get1').length,
    +      1,
    +      'get1 should only be called once'
    +    );
    +    assert.strictEqual(calls.length, 3, 'Should have exactly 3 calls total');
    +  });
    +
    +  it('calls param handlers for routes added after param() is called', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +    const calls = [];
    +
    +    router
    +      .param('id', (id, ctx, next) => {
    +        calls.push('param1');
    +        ctx.state.id = id;
    +        return next();
    +      })
    +      .get('/:id/first', (ctx) => {
    +        calls.push('get1');
    +        ctx.body = { route: 'first', id: ctx.state.id };
    +      });
    +
    +    router.get('/:id/second', (ctx) => {
    +      calls.push('get2');
    +      ctx.body = { route: 'second', id: ctx.state.id };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/123/first')
    +      .expect(200);
    +    assert.deepStrictEqual(calls, ['param1', 'get1']);
    +    assert.strictEqual(res1.body.id, '123');
    +
    +    calls.length = 0;
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/456/second')
    +      .expect(200);
    +    assert.deepStrictEqual(calls, ['param1', 'get2']);
    +    assert.strictEqual(res2.body.id, '456');
    +  });
    +
    +  it('should convert single middleware to array when adding second param handler', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +    const calls: string[] = [];
    +
    +    router.params['id'] = ((id: string, ctx: any, next: any) => {
    +      calls.push('param1');
    +      ctx.state.param1 = true;
    +      return next();
    +    }) as any;
    +
    +    router.param('id', (id: string, ctx: any, next: any) => {
    +      calls.push('param2');
    +      ctx.state.param2 = true;
    +      return next();
    +    });
    +
    +    router.get('/:id', (ctx: any) => {
    +      calls.push('get');
    +      ctx.body = {
    +        param1: ctx.state.param1,
    +        param2: ctx.state.param2
    +      };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .get('/test')
    +      .expect(200);
    +
    +    assert.deepStrictEqual(calls, ['param1', 'param2', 'get']);
    +    assert.strictEqual(res.body.param1, true);
    +    assert.strictEqual(res.body.param2, true);
    +  });
    +});
    +
    +describe('Router#opts', () => {
    +  it('responds with 200', async () => {
    +    const app = new Koa();
    +    const router = new Router({
    +      strict: true
    +    });
    +    router.get('/info', (ctx) => {
    +      ctx.body = 'hello';
    +    });
    +    const res = await request(
    +      http.createServer(app.use(router.routes()).callback())
    +    )
    +      .get('/info')
    +      .expect(200);
    +    assert.strictEqual(res.text, 'hello');
    +  });
    +
    +  it('should allow setting a prefix', async () => {
    +    const app = new Koa();
    +    const routes = new Router({ prefix: '/things/:thing_id' });
    +
    +    routes.get('/list', (ctx) => {
    +      ctx.body = ctx.params;
    +    });
    +
    +    app.use(routes.routes());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .get('/things/1/list')
    +      .expect(200);
    +    assert.strictEqual(res.body.thing_id, '1');
    +  });
    +
    +  it('responds with 404 when has a trailing slash', async () => {
    +    const app = new Koa();
    +    const router = new Router({
    +      strict: true
    +    });
    +    router.get('/info', (ctx) => {
    +      ctx.body = 'hello';
    +    });
    +    await request(http.createServer(app.use(router.routes()).callback()))
    +      .get('/info/')
    +      .expect(404);
    +  });
    +});
    +
    +describe('use middleware with opts', () => {
    +  it('responds with 200', async () => {
    +    const app = new Koa();
    +    const router = new Router({
    +      strict: true
         });
         router.get('/info', (ctx) => {
           ctx.body = 'hello';
    @@ -1816,6 +2334,29 @@ describe('router.routes()', () => {
           .expect(200);
       });
     
    +  it('sets correct `_matchedRouteName` with nested routers and middleware (gh-105)', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +    const nestedRouter = new Router();
    +
    +    router.get('main#info', '/info', (ctx) => {
    +      assert.strictEqual(ctx._matchedRouteName, 'main#info');
    +      ctx.body = { route: 'main' };
    +    });
    +
    +    nestedRouter.get('nested#updates', '/updates', (ctx) => {
    +      assert.strictEqual(ctx._matchedRouteName, 'nested#updates');
    +      ctx.body = { route: 'nested' };
    +    });
    +
    +    router.use('/v1/api', nestedRouter.routes());
    +    app.use(router.routes());
    +
    +    await request(http.createServer(app.callback()))
    +      .get('/v1/api/updates')
    +      .expect(200);
    +  });
    +
       it('places a `routerPath` value on the context for current route', async () => {
         const app = new Koa();
         const router = new Router();
    @@ -1879,6 +2420,157 @@ describe('If no HEAD method, default to GET', () => {
           .expect(200);
         assert.deepStrictEqual(res.body, {});
       });
    +
    +  it('should return empty body for HEAD requests on GET routes', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.get('/api/data', (ctx) => {
    +      ctx.body = { message: 'This is a large response', data: [1, 2, 3, 4, 5] };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const getRes = await request(http.createServer(app.callback()))
    +      .get('/api/data')
    +      .expect(200);
    +    assert.strictEqual(typeof getRes.body, 'object');
    +    assert.strictEqual(getRes.body.message, 'This is a large response');
    +
    +    const headRes = await request(http.createServer(app.callback()))
    +      .head('/api/data')
    +      .expect(200);
    +    assert.deepStrictEqual(headRes.body, {});
    +    assert.strictEqual(
    +      headRes.headers['content-type'],
    +      'application/json; charset=utf-8'
    +    );
    +  });
    +
    +  it('should preserve headers for HEAD requests', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.get('/api/resource', (ctx) => {
    +      ctx.set('X-Custom-Header', 'custom-value');
    +      ctx.set('X-Resource-Count', '42');
    +      ctx.body = 'Response body';
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .head('/api/resource')
    +      .expect(200);
    +
    +    assert.strictEqual(res.headers['x-custom-header'], 'custom-value');
    +    assert.strictEqual(res.headers['x-resource-count'], '42');
    +    assert.deepStrictEqual(res.body, {});
    +  });
    +
    +  it('should work with allowedMethods middleware', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.get('/resource', (ctx) => {
    +      ctx.body = 'success';
    +    });
    +
    +    app.use(router.routes());
    +    app.use(router.allowedMethods());
    +
    +    await request(http.createServer(app.callback()))
    +      .head('/resource')
    +      .expect(200);
    +  });
    +
    +  it('should not automatically support HEAD for POST routes', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.post('/users', (ctx) => {
    +      ctx.body = { created: true };
    +    });
    +
    +    app.use(router.routes());
    +    app.use(router.allowedMethods());
    +
    +    await request(http.createServer(app.callback())).head('/users').expect(405);
    +  });
    +
    +  it('should not automatically support HEAD for PUT routes', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.put('/users/:id', (ctx) => {
    +      ctx.body = { updated: true };
    +    });
    +
    +    app.use(router.routes());
    +    app.use(router.allowedMethods());
    +
    +    await request(http.createServer(app.callback()))
    +      .head('/users/123')
    +      .expect(405);
    +  });
    +
    +  it('should support explicit HEAD routes', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    let headCalled = false;
    +
    +    router.head('/check', (ctx) => {
    +      headCalled = true;
    +      ctx.status = 200;
    +    });
    +
    +    router.get('/check', (ctx) => {
    +      ctx.body = { message: 'GET handler' };
    +    });
    +
    +    app.use(router.routes());
    +
    +    await request(http.createServer(app.callback())).head('/check').expect(200);
    +
    +    assert.strictEqual(headCalled, true);
    +  });
    +
    +  it('should handle HEAD requests with route parameters', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.get('/users/:id/posts/:postId', (ctx) => {
    +      ctx.body = {
    +        userId: ctx.params.id,
    +        postId: ctx.params.postId
    +      };
    +    });
    +
    +    app.use(router.routes());
    +
    +    await request(http.createServer(app.callback()))
    +      .head('/users/123/posts/456')
    +      .expect(200);
    +  });
    +
    +  it('should handle HEAD requests with query parameters', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.get('/search', (ctx) => {
    +      ctx.body = {
    +        query: ctx.query.q,
    +        results: []
    +      };
    +    });
    +
    +    app.use(router.routes());
    +
    +    await request(http.createServer(app.callback()))
    +      .head('/search?q=test')
    +      .expect(200);
    +  });
     });
     
     describe('Router#prefix', () => {
    @@ -1971,6 +2663,79 @@ describe('Router#prefix', () => {
           .expect(200, /{"ping":"pingKey"}/);
       });
     
    +  it('executes middleware for routes when prefix contains path parameters (complex)', async () => {
    +    const app = new Koa();
    +
    +    const appSettingsRouter = new Router({
    +      prefix: '/api/apps/:appId/settings'
    +    });
    +
    +    let middlewareExecuted = false;
    +
    +    appSettingsRouter
    +      .use((ctx, next) => {
    +        middlewareExecuted = true;
    +        ctx.state.authorized = 'AUTHORIZED';
    +        return next();
    +      })
    +      .get('/', (ctx) => {
    +        ctx.body = {
    +          authorized: ctx.state.authorized,
    +          middlewareRan: middlewareExecuted,
    +          appId: ctx.params.appId
    +        };
    +      })
    +      .get('/:settingId', (ctx) => {
    +        ctx.body = {
    +          authorized: ctx.state.authorized,
    +          middlewareRan: middlewareExecuted,
    +          appId: ctx.params.appId,
    +          settingId: ctx.params.settingId
    +        };
    +      });
    +
    +    app.use(appSettingsRouter.routes());
    +
    +    middlewareExecuted = false;
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/api/apps/123/settings')
    +      .expect(200);
    +
    +    assert.strictEqual(
    +      res1.body.authorized,
    +      'AUTHORIZED',
    +      'Middleware should set authorized state'
    +    );
    +    assert.strictEqual(
    +      res1.body.middlewareRan,
    +      true,
    +      'Middleware should have executed'
    +    );
    +    assert.strictEqual(res1.body.appId, '123', 'Should capture appId param');
    +
    +    middlewareExecuted = false;
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/api/apps/456/settings/theme')
    +      .expect(200);
    +
    +    assert.strictEqual(
    +      res2.body.authorized,
    +      'AUTHORIZED',
    +      'Middleware should set authorized state'
    +    );
    +    assert.strictEqual(
    +      res2.body.middlewareRan,
    +      true,
    +      'Middleware should have executed'
    +    );
    +    assert.strictEqual(res2.body.appId, '456', 'Should capture appId param');
    +    assert.strictEqual(
    +      res2.body.settingId,
    +      'theme',
    +      'Should capture settingId param'
    +    );
    +  });
    +
       it('populates ctx.params correctly for static prefix', async () => {
         const app = new Koa();
         const router = new Router({ prefix: '/all' });
    @@ -2201,4 +2966,474 @@ describe('Support host', () => {
           .set('Host', 'sub.anytest.domain')
           .expect(404);
       });
    +
    +  it('should support host match with array of strings', async () => {
    +    const app = new Koa();
    +    const router = new Router({
    +      host: ['some-domain.com', 'www.some-domain.com', 'some.other-domain.com']
    +    });
    +    router.get('/', (ctx) => {
    +      ctx.body = { host: ctx.host };
    +    });
    +    app.use(router.routes());
    +    const server = http.createServer(app.callback());
    +
    +    await request(server).get('/').set('Host', 'some-domain.com').expect(200);
    +    await request(server)
    +      .get('/')
    +      .set('Host', 'www.some-domain.com')
    +      .expect(200);
    +    await request(server)
    +      .get('/')
    +      .set('Host', 'some.other-domain.com')
    +      .expect(200);
    +    await request(server).get('/').set('Host', 'other-domain.com').expect(404);
    +  });
    +
    +  it('should return false for invalid host type (neither string, array, nor RegExp)', () => {
    +    const router = new Router();
    +    (router as any).host = 123;
    +    assert.strictEqual(router.matchHost('test.domain'), false);
    +
    +    (router as any).host = {};
    +    assert.strictEqual(router.matchHost('test.domain'), false);
    +  });
    +
    +  it('should return false when input is empty or falsy', () => {
    +    const router = new Router({
    +      host: 'test.domain'
    +    });
    +
    +    assert.strictEqual(router.matchHost(''), false);
    +
    +    assert.strictEqual(router.matchHost(null as any), false);
    +
    +    assert.strictEqual(router.matchHost(undefined as any), false);
    +  });
    +
    +  it('should handle empty array for host', () => {
    +    const router = new Router({
    +      host: []
    +    });
    +    assert.strictEqual(router.matchHost('test.domain'), false);
    +  });
    +
    +  it('should handle array host matching with matchHost method', () => {
    +    const router = new Router({
    +      host: ['example.com', 'www.example.com', 'api.example.com']
    +    });
    +    assert.strictEqual(router.matchHost('example.com'), true);
    +    assert.strictEqual(router.matchHost('www.example.com'), true);
    +    assert.strictEqual(router.matchHost('api.example.com'), true);
    +    assert.strictEqual(router.matchHost('other.com'), false);
    +    assert.strictEqual(router.matchHost(''), false);
    +  });
    +});
    +
    +describe('Less Common HTTP Methods', () => {
    +  it('should support PATCH method', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.patch('/users/:id', async (ctx) => {
    +      ctx.body = {
    +        id: ctx.params.id,
    +        message: 'User partially updated',
    +        method: 'PATCH'
    +      };
    +    });
    +
    +    app.use(router.routes());
    +    app.use(router.allowedMethods());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .patch('/users/123')
    +      .expect(200);
    +
    +    assert.strictEqual(res.body.id, '123');
    +    assert.strictEqual(res.body.method, 'PATCH');
    +    assert.strictEqual(res.body.message, 'User partially updated');
    +  });
    +
    +  it('should support PURGE method', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +    let cacheCleared = false;
    +
    +    router.purge('/cache/:key', async (ctx) => {
    +      cacheCleared = true;
    +      ctx.body = {
    +        key: ctx.params.key,
    +        message: 'Cache cleared',
    +        method: 'PURGE'
    +      };
    +    });
    +
    +    app.use(router.routes());
    +    app.use(router.allowedMethods());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .purge('/cache/mykey')
    +      .expect(200);
    +
    +    assert.strictEqual(cacheCleared, true);
    +    assert.strictEqual(res.body.key, 'mykey');
    +    assert.strictEqual(res.body.method, 'PURGE');
    +  });
    +
    +  it('should support COPY method', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.copy('/files/:source', async (ctx) => {
    +      ctx.body = {
    +        source: ctx.params.source,
    +        message: 'File copied',
    +        method: 'COPY'
    +      };
    +    });
    +
    +    app.use(router.routes());
    +    app.use(router.allowedMethods());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .copy('/files/document.pdf')
    +      .expect(200);
    +
    +    assert.strictEqual(res.body.source, 'document.pdf');
    +    assert.strictEqual(res.body.method, 'COPY');
    +  });
    +
    +  it('should support CONNECT method', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.connect('/proxy/:host', async (ctx) => {
    +      ctx.body = {
    +        host: ctx.params.host,
    +        message: 'Connection established',
    +        method: 'CONNECT'
    +      };
    +    });
    +
    +    app.use(router.routes());
    +    app.use(router.allowedMethods());
    +
    +    assert.strictEqual(typeof router.connect, 'function');
    +  });
    +
    +  it('should support TRACE method', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.trace('/debug/:path', async (ctx) => {
    +      ctx.body = {
    +        path: ctx.params.path,
    +        message: 'Trace completed',
    +        method: 'TRACE'
    +      };
    +    });
    +
    +    app.use(router.routes());
    +    app.use(router.allowedMethods());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .trace('/debug/test')
    +      .expect(200);
    +
    +    assert.strictEqual(res.body.path, 'test');
    +    assert.strictEqual(res.body.method, 'TRACE');
    +  });
    +});
    +
    +describe('RouterOptions: methods', () => {
    +  it('should limit router to specified methods only', async () => {
    +    const app = new Koa();
    +    const router = new Router({
    +      methods: ['GET', 'POST', 'PATCH']
    +    });
    +
    +    router.get('/users', (ctx) => {
    +      ctx.body = { method: 'GET' };
    +    });
    +
    +    router.post('/users', (ctx) => {
    +      ctx.body = { method: 'POST' };
    +    });
    +
    +    router.patch('/users/:id', (ctx) => {
    +      ctx.body = { method: 'PATCH', id: ctx.params.id };
    +    });
    +
    +    app.use(router.routes());
    +    app.use(router.allowedMethods());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/users')
    +      .expect(200);
    +    assert.strictEqual(res1.body.method, 'GET');
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .post('/users')
    +      .expect(200);
    +    assert.strictEqual(res2.body.method, 'POST');
    +
    +    const res3 = await request(http.createServer(app.callback()))
    +      .patch('/users/123')
    +      .expect(200);
    +    assert.strictEqual(res3.body.method, 'PATCH');
    +
    +    await request(http.createServer(app.callback()))
    +      .put('/users/123')
    +      .expect(501);
    +
    +    await request(http.createServer(app.callback()))
    +      .delete('/users/123')
    +      .expect(501);
    +  });
    +
    +  it('should allow PURGE when included in methods array', async () => {
    +    const app = new Koa();
    +    const router = new Router({
    +      methods: ['GET', 'POST', 'PURGE']
    +    });
    +
    +    router.purge('/cache/:key', (ctx) => {
    +      ctx.body = { key: ctx.params.key, cleared: true };
    +    });
    +
    +    app.use(router.routes());
    +    app.use(router.allowedMethods());
    +
    +    const res = await request(http.createServer(app.callback()))
    +      .purge('/cache/test')
    +      .expect(200);
    +
    +    assert.strictEqual(res.body.key, 'test');
    +    assert.strictEqual(res.body.cleared, true);
    +  });
    +
    +  it('should return 501 when method not in methods array', async () => {
    +    const app = new Koa();
    +    const router = new Router({
    +      methods: ['GET', 'POST', 'PUT', 'DELETE']
    +    });
    +
    +    router.get('/cache/:key', (ctx) => {
    +      ctx.body = { key: ctx.params.key, method: 'GET' };
    +    });
    +
    +    router.purge('/cache/:key', (ctx) => {
    +      ctx.body = { key: ctx.params.key, method: 'PURGE' };
    +    });
    +
    +    app.use(router.routes());
    +    app.use(router.allowedMethods());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/cache/test')
    +      .expect(200);
    +    assert.strictEqual(res1.body.method, 'GET');
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .purge('/nonexistent')
    +      .expect(501);
    +  });
    +});
    +
    +describe('RouterOptions: sensitive', () => {
    +  it('should enable case-sensitive routing when sensitive is true', async () => {
    +    const app = new Koa();
    +    const router = new Router({
    +      sensitive: true
    +    });
    +
    +    router.get('/Users', (ctx) => {
    +      ctx.body = { path: '/Users', caseSensitive: true };
    +    });
    +
    +    router.get('/users', (ctx) => {
    +      ctx.body = { path: '/users', caseSensitive: true };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/Users')
    +      .expect(200);
    +    assert.strictEqual(res1.body.path, '/Users');
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/users')
    +      .expect(200);
    +    assert.strictEqual(res2.body.path, '/users');
    +
    +    await request(http.createServer(app.callback())).get('/USERS').expect(404);
    +  });
    +
    +  it('should use case-insensitive routing by default', async () => {
    +    const app = new Koa();
    +    const router = new Router();
    +
    +    router.get('/Users', (ctx) => {
    +      ctx.body = { path: '/Users' };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/Users')
    +      .expect(200);
    +    assert.strictEqual(res1.body.path, '/Users');
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/users')
    +      .expect(200);
    +    assert.strictEqual(res2.body.path, '/Users');
    +
    +    const res3 = await request(http.createServer(app.callback()))
    +      .get('/USERS')
    +      .expect(200);
    +    assert.strictEqual(res3.body.path, '/Users');
    +  });
    +
    +  it('should apply sensitive option to nested routers', async () => {
    +    const app = new Koa();
    +    const parentRouter = new Router({
    +      sensitive: true
    +    });
    +
    +    const nestedRouter = new Router({
    +      sensitive: true
    +    });
    +
    +    nestedRouter.get('/Items', (ctx) => {
    +      ctx.body = { path: '/Items' };
    +    });
    +
    +    parentRouter.use('/Api', nestedRouter.routes());
    +    app.use(parentRouter.routes());
    +
    +    await request(http.createServer(app.callback()))
    +      .get('/Api/Items')
    +      .expect(200);
    +
    +    await request(http.createServer(app.callback()))
    +      .get('/api/items')
    +      .expect(404);
    +  });
    +});
    +
    +describe('RouterOptions: strict (comprehensive)', () => {
    +  it('should require trailing slash when strict is true', async () => {
    +    const app = new Koa();
    +    const router = new Router({
    +      strict: true
    +    });
    +
    +    router.get('/info', (ctx) => {
    +      ctx.body = { path: '/info', strict: true };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/info')
    +      .expect(200);
    +    assert.strictEqual(res1.body.path, '/info');
    +
    +    await request(http.createServer(app.callback())).get('/info/').expect(404);
    +  });
    +
    +  it('should allow trailing slash when strict is false', async () => {
    +    const app = new Koa();
    +    const router = new Router({ strict: false });
    +
    +    router.get('/info', (ctx) => {
    +      ctx.body = { path: '/info', strict: false };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/info')
    +      .expect(200);
    +    assert.strictEqual(res1.body.path, '/info');
    +
    +    const res2 = await request(http.createServer(app.callback()))
    +      .get('/info/')
    +      .expect(200);
    +    assert.strictEqual(res2.body.path, '/info');
    +  });
    +
    +  it('should apply strict option to routes with parameters', async () => {
    +    const app = new Koa();
    +    const router = new Router({
    +      strict: true
    +    });
    +
    +    router.get('/users/:id', (ctx) => {
    +      ctx.body = { id: ctx.params.id };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/users/123')
    +      .expect(200);
    +    assert.strictEqual(res1.body.id, '123');
    +
    +    await request(http.createServer(app.callback()))
    +      .get('/users/123/')
    +      .expect(404);
    +  });
    +
    +  it('should apply strict option to nested routers', async () => {
    +    const app = new Koa();
    +    const parentRouter = new Router({
    +      strict: true
    +    });
    +
    +    const nestedRouter = new Router({
    +      strict: true
    +    });
    +
    +    nestedRouter.get('/items', (ctx) => {
    +      ctx.body = { path: '/items' };
    +    });
    +
    +    parentRouter.use('/api', nestedRouter.routes());
    +    app.use(parentRouter.routes());
    +
    +    await request(http.createServer(app.callback()))
    +      .get('/api/items')
    +      .expect(200);
    +
    +    await request(http.createServer(app.callback()))
    +      .get('/api/items/')
    +      .expect(404);
    +  });
    +
    +  it('should handle strict option with prefix', async () => {
    +    const app = new Koa();
    +    const router = new Router({
    +      prefix: '/v1',
    +      strict: true
    +    });
    +
    +    router.get('/users', (ctx) => {
    +      ctx.body = { path: '/v1/users' };
    +    });
    +
    +    app.use(router.routes());
    +
    +    const res1 = await request(http.createServer(app.callback()))
    +      .get('/v1/users')
    +      .expect(200);
    +    assert.strictEqual(res1.body.path, '/v1/users');
    +
    +    await request(http.createServer(app.callback()))
    +      .get('/v1/users/')
    +      .expect(404);
    +  });
     });
    
  • test/utils/http-methods.test.ts+107 0 added
    @@ -0,0 +1,107 @@
    +/**
    + * Tests for HTTP methods utilities
    + */
    +
    +import { describe, it } from 'node:test';
    +import assert from 'node:assert';
    +import http from 'node:http';
    +import {
    +  getAllHttpMethods,
    +  COMMON_HTTP_METHODS
    +} from '../../src/utils/http-methods';
    +
    +describe('http-methods utilities', () => {
    +  describe('getAllHttpMethods()', () => {
    +    it('should return all HTTP methods in lowercase', () => {
    +      const methods = getAllHttpMethods();
    +
    +      assert.strictEqual(Array.isArray(methods), true);
    +      assert.strictEqual(methods.length > 0, true);
    +
    +      for (const method of methods) {
    +        assert.strictEqual(typeof method, 'string');
    +        assert.strictEqual(
    +          method,
    +          method.toLowerCase(),
    +          `Method ${method} should be lowercase`
    +        );
    +      }
    +    });
    +
    +    it('should return methods from Node.js http.METHODS', () => {
    +      const methods = getAllHttpMethods();
    +      const nodeMethods = http.METHODS.map((m) => m.toLowerCase());
    +
    +      assert.strictEqual(methods.length, nodeMethods.length);
    +      assert.deepStrictEqual(methods.sort(), nodeMethods.sort());
    +    });
    +
    +    it('should include common HTTP methods', () => {
    +      const methods = getAllHttpMethods();
    +
    +      assert.strictEqual(methods.includes('get'), true);
    +      assert.strictEqual(methods.includes('post'), true);
    +      assert.strictEqual(methods.includes('put'), true);
    +      assert.strictEqual(methods.includes('patch'), true);
    +      assert.strictEqual(methods.includes('delete'), true);
    +      assert.strictEqual(methods.includes('head'), true);
    +      assert.strictEqual(methods.includes('options'), true);
    +    });
    +
    +    it('should include less common HTTP methods', () => {
    +      const methods = getAllHttpMethods();
    +
    +      assert.strictEqual(methods.includes('connect'), true);
    +      assert.strictEqual(methods.includes('trace'), true);
    +      assert.strictEqual(methods.includes('purge'), true);
    +      assert.strictEqual(methods.includes('copy'), true);
    +    });
    +  });
    +
    +  describe('COMMON_HTTP_METHODS', () => {
    +    it('should be an array of strings', () => {
    +      assert.strictEqual(Array.isArray(COMMON_HTTP_METHODS), true);
    +      assert.strictEqual(COMMON_HTTP_METHODS.length > 0, true);
    +
    +      for (const method of COMMON_HTTP_METHODS) {
    +        assert.strictEqual(typeof method, 'string');
    +      }
    +    });
    +
    +    it('should contain expected common methods', () => {
    +      const expectedMethods = [
    +        'get',
    +        'post',
    +        'put',
    +        'patch',
    +        'delete',
    +        'del',
    +        'head',
    +        'options'
    +      ];
    +
    +      for (const expected of expectedMethods) {
    +        assert.strictEqual(
    +          COMMON_HTTP_METHODS.includes(expected),
    +          true,
    +          `COMMON_HTTP_METHODS should include ${expected}`
    +        );
    +      }
    +    });
    +
    +    it('should have exactly 8 common methods', () => {
    +      assert.strictEqual(COMMON_HTTP_METHODS.length, 8);
    +    });
    +
    +    it('should include both delete and del', () => {
    +      assert.strictEqual(COMMON_HTTP_METHODS.includes('delete'), true);
    +      assert.strictEqual(COMMON_HTTP_METHODS.includes('del'), true);
    +    });
    +
    +    it('should not include less common methods', () => {
    +      assert.strictEqual(COMMON_HTTP_METHODS.includes('connect'), false);
    +      assert.strictEqual(COMMON_HTTP_METHODS.includes('trace'), false);
    +      assert.strictEqual(COMMON_HTTP_METHODS.includes('purge'), false);
    +    });
    +  });
    +});
    
  • test/utils/parameter-helpers.test.ts+316 0 added
    @@ -0,0 +1,316 @@
    +/**
    + * Tests for parameter handling utilities
    + */
    +
    +import { describe, it } from 'node:test';
    +import assert from 'node:assert';
    +import Router from '../../src/router';
    +import Layer from '../../src/layer';
    +import {
    +  normalizeParameterMiddleware,
    +  applyParameterMiddlewareToRoute,
    +  applyAllParameterMiddleware
    +} from '../../src/utils/parameter-helpers';
    +import type { RouterParameterMiddleware } from '../../src/types';
    +
    +describe('parameter-helpers utilities', () => {
    +  describe('normalizeParameterMiddleware()', () => {
    +    it('should return empty array for undefined', () => {
    +      const result = normalizeParameterMiddleware(undefined);
    +      assert.deepStrictEqual(result, []);
    +    });
    +
    +    it('should return array for single middleware function', () => {
    +      const middleware: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        return next();
    +      };
    +
    +      const result = normalizeParameterMiddleware(middleware);
    +      assert.strictEqual(Array.isArray(result), true);
    +      assert.strictEqual(result.length, 1);
    +      assert.strictEqual(result[0], middleware);
    +    });
    +
    +    it('should return array as-is for array input', () => {
    +      const middleware1: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        return next();
    +      };
    +      const middleware2: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        return next();
    +      };
    +      const middlewareArray = [middleware1, middleware2];
    +
    +      const result = normalizeParameterMiddleware(middlewareArray);
    +      assert.strictEqual(Array.isArray(result), true);
    +      assert.strictEqual(result.length, 2);
    +      assert.deepStrictEqual(result, middlewareArray);
    +    });
    +
    +    it('should handle empty array', () => {
    +      const result = normalizeParameterMiddleware([]);
    +      assert.deepStrictEqual(result, []);
    +    });
    +
    +    it('should handle single item array', () => {
    +      const middleware: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        return next();
    +      };
    +      const result = normalizeParameterMiddleware([middleware]);
    +      assert.strictEqual(result.length, 1);
    +      assert.strictEqual(result[0], middleware);
    +    });
    +  });
    +
    +  describe('applyParameterMiddlewareToRoute()', () => {
    +    it('should apply single middleware to router', () => {
    +      const router = new Router();
    +
    +      const middleware: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        return next();
    +      };
    +
    +      applyParameterMiddlewareToRoute(router, 'id', middleware);
    +
    +      assert.strictEqual(router.params.id !== undefined, true);
    +      assert.strictEqual(Array.isArray(router.params.id), true);
    +      const registered = router.params.id as RouterParameterMiddleware[];
    +      assert.strictEqual(registered.length, 1);
    +      assert.strictEqual(registered[0], middleware);
    +    });
    +
    +    it('should apply array of middleware to router', () => {
    +      const router = new Router();
    +      const callOrder: string[] = [];
    +
    +      const middleware1: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        callOrder.push('1');
    +        return next();
    +      };
    +      const middleware2: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        callOrder.push('2');
    +        return next();
    +      };
    +
    +      applyParameterMiddlewareToRoute(router, 'id', [middleware1, middleware2]);
    +
    +      assert.strictEqual(Array.isArray(router.params.id), true);
    +      const registered = router.params.id as RouterParameterMiddleware[];
    +      assert.strictEqual(registered.length, 2);
    +      assert.strictEqual(registered[0], middleware1);
    +      assert.strictEqual(registered[1], middleware2);
    +    });
    +
    +    it('should apply middleware to Layer', () => {
    +      const layer = new Layer('/users/:id', ['GET'], async () => {});
    +      let called = false;
    +
    +      const middleware: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        called = true;
    +        return next();
    +      };
    +
    +      applyParameterMiddlewareToRoute(layer, 'id', middleware);
    +
    +      assert.strictEqual(typeof middleware, 'function');
    +    });
    +
    +    it('should handle undefined middleware gracefully', () => {
    +      const router = new Router();
    +
    +      applyParameterMiddlewareToRoute(router, 'id', undefined as any);
    +
    +      assert.strictEqual(router instanceof Router, true);
    +    });
    +  });
    +
    +  describe('applyAllParameterMiddleware()', () => {
    +    it('should apply all middleware from params object', () => {
    +      const router = new Router();
    +      const callOrder: string[] = [];
    +
    +      const idMiddleware: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        callOrder.push('id');
    +        return next();
    +      };
    +
    +      const nameMiddleware: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        callOrder.push('name');
    +        return next();
    +      };
    +
    +      const paramsObject = {
    +        id: idMiddleware,
    +        name: nameMiddleware
    +      };
    +
    +      const layer = new Layer('/users/:id/:name', ['GET'], async () => {});
    +
    +      applyAllParameterMiddleware(layer, paramsObject);
    +
    +      assert.strictEqual(typeof idMiddleware, 'function');
    +      assert.strictEqual(typeof nameMiddleware, 'function');
    +    });
    +
    +    it('should handle empty params object', () => {
    +      const layer = new Layer('/users', ['GET'], async () => {});
    +
    +      applyAllParameterMiddleware(layer, {});
    +
    +      assert.strictEqual(layer instanceof Layer, true);
    +    });
    +
    +    it('should handle params object with array middleware', () => {
    +      const router = new Router();
    +
    +      const middleware1: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        return next();
    +      };
    +      const middleware2: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        return next();
    +      };
    +
    +      const paramsObject = {
    +        id: [middleware1, middleware2] as RouterParameterMiddleware[]
    +      };
    +
    +      const layer = new Layer('/users/:id', ['GET'], async () => {});
    +
    +      applyAllParameterMiddleware(layer, paramsObject);
    +
    +      assert.strictEqual(layer instanceof Layer, true);
    +    });
    +
    +    it('should apply middleware for multiple parameters', () => {
    +      const router = new Router();
    +
    +      const idMiddleware: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        return next();
    +      };
    +
    +      const userIdMiddleware: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        return next();
    +      };
    +
    +      const postIdMiddleware: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        return next();
    +      };
    +
    +      const paramsObject = {
    +        id: idMiddleware,
    +        userId: userIdMiddleware,
    +        postId: postIdMiddleware
    +      };
    +
    +      const layer = new Layer(
    +        '/users/:userId/posts/:postId',
    +        ['GET'],
    +        async () => {}
    +      );
    +
    +      applyAllParameterMiddleware(layer, paramsObject);
    +
    +      assert.strictEqual(layer instanceof Layer, true);
    +    });
    +
    +    it('should handle mixed single and array middleware', () => {
    +      const router = new Router();
    +
    +      const singleMiddleware: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        return next();
    +      };
    +
    +      const arrayMiddleware1: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        return next();
    +      };
    +      const arrayMiddleware2: RouterParameterMiddleware = async (
    +        value,
    +        ctx,
    +        next
    +      ) => {
    +        return next();
    +      };
    +
    +      const paramsObject = {
    +        id: singleMiddleware,
    +        userId: [
    +          arrayMiddleware1,
    +          arrayMiddleware2
    +        ] as RouterParameterMiddleware[]
    +      };
    +
    +      const layer = new Layer('/users/:userId/:id', ['GET'], async () => {});
    +
    +      applyAllParameterMiddleware(layer, paramsObject);
    +
    +      assert.strictEqual(layer instanceof Layer, true);
    +    });
    +  });
    +});
    
  • test/utils/path-helpers.test.ts+195 0 added
    @@ -0,0 +1,195 @@
    +/**
    + * Tests for path handling utilities
    + */
    +
    +import { describe, it } from 'node:test';
    +import assert from 'node:assert';
    +import {
    +  hasPathParameters,
    +  determineMiddlewarePath
    +} from '../../src/utils/path-helpers';
    +import type { LayerOptions } from '../../src/types';
    +
    +describe('path-helpers utilities', () => {
    +  describe('hasPathParameters()', () => {
    +    it('should return false for empty string', () => {
    +      assert.strictEqual(hasPathParameters(''), false);
    +    });
    +
    +    it('should return false for undefined', () => {
    +      assert.strictEqual(hasPathParameters(undefined as any), false);
    +    });
    +
    +    it('should return false for path without parameters', () => {
    +      assert.strictEqual(hasPathParameters('/users'), false);
    +      assert.strictEqual(hasPathParameters('/api/v1'), false);
    +      assert.strictEqual(hasPathParameters('/'), false);
    +    });
    +
    +    it('should return true for path with single parameter', () => {
    +      assert.strictEqual(hasPathParameters('/users/:id'), true);
    +      assert.strictEqual(hasPathParameters('/:category'), true);
    +    });
    +
    +    it('should return true for path with multiple parameters', () => {
    +      assert.strictEqual(
    +        hasPathParameters('/users/:userId/posts/:postId'),
    +        true
    +      );
    +      assert.strictEqual(hasPathParameters('/:category/:title'), true);
    +    });
    +
    +    it('should return true for path with optional parameter', () => {
    +      assert.strictEqual(hasPathParameters('/user/:id'), true);
    +    });
    +
    +    it('should return true for path with wildcard parameter', () => {
    +      assert.strictEqual(hasPathParameters('/files/{/*path}'), true);
    +    });
    +
    +    it('should return true for prefix with parameters', () => {
    +      assert.strictEqual(hasPathParameters('/api/v:version'), true);
    +      assert.strictEqual(hasPathParameters('/users/:userId'), true);
    +    });
    +
    +    it('should handle options parameter', () => {
    +      const options: LayerOptions = {
    +        sensitive: true,
    +        strict: false
    +      };
    +
    +      assert.strictEqual(hasPathParameters('/users/:id', options), true);
    +      assert.strictEqual(hasPathParameters('/users', options), false);
    +    });
    +  });
    +
    +  describe('determineMiddlewarePath()', () => {
    +    describe('with explicit path provided', () => {
    +      it('should return empty string as wildcard when explicit path is empty string', () => {
    +        const result = determineMiddlewarePath('', false);
    +
    +        assert.strictEqual(result.path, '{/*rest}');
    +        assert.strictEqual(result.pathAsRegExp, false);
    +      });
    +
    +      it('should return root path as-is', () => {
    +        const result = determineMiddlewarePath('/', false);
    +
    +        assert.strictEqual(result.path, '/');
    +        assert.strictEqual(result.pathAsRegExp, false);
    +      });
    +
    +      it('should return string path as-is', () => {
    +        const result = determineMiddlewarePath('/api', false);
    +
    +        assert.strictEqual(result.path, '/api');
    +        assert.strictEqual(result.pathAsRegExp, false);
    +      });
    +
    +      it('should return RegExp path with pathAsRegExp flag', () => {
    +        const regexp = /^\/api\//;
    +        const result = determineMiddlewarePath(regexp, false);
    +
    +        assert.strictEqual(result.path, regexp);
    +        assert.strictEqual(result.pathAsRegExp, true);
    +      });
    +
    +      it('should handle nested paths', () => {
    +        const result = determineMiddlewarePath('/api/v1/users', false);
    +
    +        assert.strictEqual(result.path, '/api/v1/users');
    +        assert.strictEqual(result.pathAsRegExp, false);
    +      });
    +
    +      it('should handle paths with parameters', () => {
    +        const result = determineMiddlewarePath('/users/:id', false);
    +
    +        assert.strictEqual(result.path, '/users/:id');
    +        assert.strictEqual(result.pathAsRegExp, false);
    +      });
    +    });
    +
    +    describe('without explicit path', () => {
    +      it('should return wildcard when prefix has parameters', () => {
    +        const result = determineMiddlewarePath(undefined, true);
    +
    +        assert.strictEqual(result.path, '{/*rest}');
    +        assert.strictEqual(result.pathAsRegExp, false);
    +      });
    +
    +      it('should return regex boundary when prefix has no parameters', () => {
    +        const result = determineMiddlewarePath(undefined, false);
    +
    +        assert.strictEqual(result.pathAsRegExp, true);
    +        assert.strictEqual(typeof result.path, 'string');
    +        assert.strictEqual(
    +          result.path.includes('\\/'),
    +          true || result.path.includes('|')
    +        );
    +      });
    +
    +      it('should return boundary regex pattern for default case', () => {
    +        const result = determineMiddlewarePath(undefined, false);
    +
    +        assert.strictEqual(result.pathAsRegExp, true);
    +        const pattern = result.path as string;
    +        assert.strictEqual(typeof pattern, 'string');
    +      });
    +    });
    +
    +    describe('edge cases', () => {
    +      it('should handle empty string with prefix parameters', () => {
    +        const result = determineMiddlewarePath('', true);
    +
    +        assert.strictEqual(result.path, '{/*rest}');
    +        assert.strictEqual(result.pathAsRegExp, false);
    +      });
    +
    +      it('should handle root path with prefix parameters', () => {
    +        const result = determineMiddlewarePath('/', true);
    +
    +        assert.strictEqual(result.path, '/');
    +        assert.strictEqual(result.pathAsRegExp, false);
    +      });
    +
    +      it('should handle RegExp with prefix parameters', () => {
    +        const regexp = /^\/api\//;
    +        const result = determineMiddlewarePath(regexp, true);
    +
    +        assert.strictEqual(result.path, regexp);
    +        assert.strictEqual(result.pathAsRegExp, true);
    +      });
    +
    +      it('should handle complex RegExp patterns', () => {
    +        const regexp = /^\/api\/v\d+\//;
    +        const result = determineMiddlewarePath(regexp, false);
    +
    +        assert.strictEqual(result.path, regexp);
    +        assert.strictEqual(result.pathAsRegExp, true);
    +      });
    +    });
    +
    +    describe('integration scenarios', () => {
    +      it('should handle middleware path for nested routers', () => {
    +        const result = determineMiddlewarePath('/api', false);
    +
    +        assert.strictEqual(result.path, '/api');
    +        assert.strictEqual(result.pathAsRegExp, false);
    +      });
    +
    +      it('should handle middleware path for parameterized prefix', () => {
    +        const result = determineMiddlewarePath(undefined, true);
    +
    +        assert.strictEqual(result.path, '{/*rest}');
    +        assert.strictEqual(result.pathAsRegExp, false);
    +      });
    +
    +      it('should handle explicit path override for parameterized prefix', () => {
    +        const result = determineMiddlewarePath('/posts', true);
    +
    +        assert.strictEqual(result.path, '/posts');
    +        assert.strictEqual(result.pathAsRegExp, false);
    +      });
    +    });
    +  });
    +});
    
  • test/utils/path-to-regexp-wrapper.test.ts+394 0 added
    @@ -0,0 +1,394 @@
    +/**
    + * Tests for path-to-regexp wrapper utilities
    + */
    +
    +import { describe, it } from 'node:test';
    +import assert from 'node:assert';
    +import {
    +  compilePathToRegexp,
    +  compilePath,
    +  parsePath,
    +  normalizeLayerOptionsToPathToRegexp,
    +  type Key
    +} from '../../src/utils/path-to-regexp-wrapper';
    +import type { LayerOptions } from '../../src/types';
    +
    +describe('path-to-regexp-wrapper utilities', () => {
    +  describe('compilePathToRegexp()', () => {
    +    it('should compile a simple path without parameters', () => {
    +      const result = compilePathToRegexp('/users');
    +
    +      assert.strictEqual(result.regexp instanceof RegExp, true);
    +      assert.strictEqual(result.keys.length, 0);
    +      assert.strictEqual(result.regexp.test('/users'), true);
    +      assert.strictEqual(result.regexp.test('/users/123'), false);
    +    });
    +
    +    it('should compile a path with single parameter', () => {
    +      const result = compilePathToRegexp('/users/:id');
    +
    +      assert.strictEqual(result.regexp instanceof RegExp, true);
    +      assert.strictEqual(result.keys.length, 1);
    +      assert.strictEqual(result.keys[0].name, 'id');
    +      assert.strictEqual(result.regexp.test('/users/123'), true);
    +      assert.strictEqual(result.regexp.test('/users'), false);
    +    });
    +
    +    it('should compile a path with multiple parameters', () => {
    +      const result = compilePathToRegexp('/users/:userId/posts/:postId');
    +
    +      assert.strictEqual(result.regexp instanceof RegExp, true);
    +      assert.strictEqual(result.keys.length, 2);
    +      assert.strictEqual(result.keys[0].name, 'userId');
    +      assert.strictEqual(result.keys[1].name, 'postId');
    +      assert.strictEqual(result.regexp.test('/users/123/posts/456'), true);
    +    });
    +
    +    it('should handle case-sensitive option', () => {
    +      const result1 = compilePathToRegexp('/Users', { sensitive: false });
    +      const result2 = compilePathToRegexp('/Users', { sensitive: true });
    +
    +      assert.strictEqual(result1.regexp.test('/users'), true);
    +      assert.strictEqual(result2.regexp.test('/users'), false);
    +      assert.strictEqual(result2.regexp.test('/Users'), true);
    +    });
    +
    +    it('should handle strict/trailing option conversion', () => {
    +      const result1 = compilePathToRegexp('/users/', { strict: true });
    +      assert.strictEqual(result1.regexp.test('/users/'), true);
    +
    +      const result2 = compilePathToRegexp('/users/', { strict: false });
    +      assert.strictEqual(result2.regexp.test('/users/'), true);
    +    });
    +
    +    it('should handle trailing option directly', () => {
    +      const result1 = compilePathToRegexp('/users/', { trailing: false });
    +      assert.strictEqual(result1.regexp.test('/users/'), true);
    +
    +      const result2 = compilePathToRegexp('/users/', { trailing: true });
    +      assert.strictEqual(result2.regexp.test('/users/'), true);
    +    });
    +
    +    it('should handle end option', () => {
    +      const result1 = compilePathToRegexp('/users', { end: true });
    +      assert.strictEqual(result1.regexp.test('/users'), true);
    +      assert.strictEqual(result1.regexp.test('/users/123'), false);
    +
    +      const result2 = compilePathToRegexp('/users', { end: false });
    +      assert.strictEqual(result2.regexp.test('/users'), true);
    +      assert.strictEqual(result2.regexp.test('/users/123'), true);
    +    });
    +
    +    it('should remove LayerOptions-specific properties', () => {
    +      const options: LayerOptions = {
    +        pathAsRegExp: true,
    +        ignoreCaptures: true,
    +        prefix: '/api',
    +        sensitive: true,
    +        end: true
    +      };
    +
    +      const result = compilePathToRegexp('/users/:id', options);
    +      assert.strictEqual(result.keys.length, 1);
    +      assert.strictEqual(result.keys[0].name, 'id');
    +    });
    +
    +    it('should handle wildcard paths', () => {
    +      const result = compilePathToRegexp('/files/{/*path}');
    +
    +      assert.strictEqual(result.regexp instanceof RegExp, true);
    +      assert.strictEqual(result.regexp.test('/files/'), true);
    +    });
    +
    +    it('should handle root path', () => {
    +      const result = compilePathToRegexp('/');
    +
    +      assert.strictEqual(result.regexp instanceof RegExp, true);
    +      assert.strictEqual(result.keys.length, 0);
    +      assert.strictEqual(result.regexp.test('/'), true);
    +    });
    +
    +    it('should handle empty options object', () => {
    +      const result = compilePathToRegexp('/users/:id', {});
    +
    +      assert.strictEqual(result.regexp instanceof RegExp, true);
    +      assert.strictEqual(result.keys.length, 1);
    +    });
    +  });
    +
    +  describe('compilePath()', () => {
    +    it('should compile a path to a URL generator function', () => {
    +      const urlGenerator = compilePath('/users/:id');
    +
    +      assert.strictEqual(typeof urlGenerator, 'function');
    +      const url = urlGenerator({ id: '123' });
    +      assert.strictEqual(url, '/users/123');
    +    });
    +
    +    it('should handle multiple parameters', () => {
    +      const urlGenerator = compilePath('/users/:userId/posts/:postId');
    +
    +      const url = urlGenerator({ userId: '123', postId: '456' });
    +      assert.strictEqual(url, '/users/123/posts/456');
    +    });
    +
    +    it('should handle encode option', () => {
    +      const urlGenerator = compilePath('/users/:id', {
    +        encode: encodeURIComponent
    +      });
    +
    +      const url = urlGenerator({ id: 'user name' });
    +      assert.strictEqual(url, '/users/user%20name');
    +    });
    +
    +    it('should handle custom encode function', () => {
    +      const customEncode = (value: string) => value.toUpperCase();
    +      const urlGenerator = compilePath('/users/:id', { encode: customEncode });
    +
    +      const url = urlGenerator({ id: 'test' });
    +      assert.strictEqual(url, '/users/TEST');
    +    });
    +
    +    it('should handle paths without parameters', () => {
    +      const urlGenerator = compilePath('/users');
    +
    +      const url = urlGenerator();
    +      assert.strictEqual(url, '/users');
    +    });
    +
    +    it('should handle optional parameters', () => {
    +      const urlGenerator = compilePath('/users{/:id}');
    +
    +      const url1 = urlGenerator({ id: '123' });
    +      assert.strictEqual(url1, '/users/123');
    +
    +      const url2 = urlGenerator({});
    +      assert.strictEqual(typeof url2, 'string');
    +      assert.strictEqual(url2, '/users');
    +
    +      const url3 = urlGenerator();
    +      assert.strictEqual(typeof url3, 'string');
    +      assert.strictEqual(url3, '/users');
    +    });
    +  });
    +
    +  describe('parsePath()', () => {
    +    it('should parse a simple path', () => {
    +      const result = parsePath('/users');
    +
    +      const tokens = Array.isArray(result)
    +        ? result
    +        : (result as any).tokens || [];
    +      assert.strictEqual(tokens.length > 0, true);
    +    });
    +
    +    it('should parse a path with parameters', () => {
    +      const result = parsePath('/users/:id');
    +
    +      const tokens = Array.isArray(result)
    +        ? result
    +        : (result as any).tokens || [];
    +      assert.strictEqual(tokens.length > 0, true);
    +      const hasParam = tokens.some(
    +        (token: any) =>
    +          token &&
    +          typeof token === 'object' &&
    +          'name' in token &&
    +          token.name === 'id'
    +      );
    +      assert.strictEqual(hasParam, true);
    +    });
    +
    +    it('should parse a path with multiple parameters', () => {
    +      const result = parsePath('/users/:userId/posts/:postId');
    +
    +      const tokens = Array.isArray(result)
    +        ? result
    +        : (result as any).tokens || [];
    +      assert.strictEqual(tokens.length > 0, true);
    +      const userIdToken = tokens.find(
    +        (token: any) =>
    +          token &&
    +          typeof token === 'object' &&
    +          'name' in token &&
    +          token.name === 'userId'
    +      );
    +      const postIdToken = tokens.find(
    +        (token: any) =>
    +          token &&
    +          typeof token === 'object' &&
    +          'name' in token &&
    +          token.name === 'postId'
    +      );
    +
    +      assert.strictEqual(userIdToken !== undefined, true);
    +      assert.strictEqual(postIdToken !== undefined, true);
    +    });
    +
    +    it('should parse wildcard paths', () => {
    +      const result = parsePath('/files/{/*path}');
    +
    +      const tokens = Array.isArray(result)
    +        ? result
    +        : (result as any).tokens || [];
    +      assert.strictEqual(tokens.length > 0, true);
    +    });
    +
    +    it('should parse root path', () => {
    +      const result = parsePath('/');
    +
    +      const tokens = Array.isArray(result)
    +        ? result
    +        : (result as any).tokens || [];
    +      assert.strictEqual(tokens.length > 0, true);
    +    });
    +
    +    it('should handle options parameter', () => {
    +      const result = parsePath('/users/:id', {});
    +
    +      const tokens = Array.isArray(result)
    +        ? result
    +        : (result as any).tokens || [];
    +      assert.strictEqual(tokens.length > 0, true);
    +    });
    +  });
    +
    +  describe('normalizeLayerOptionsToPathToRegexp()', () => {
    +    it('should normalize basic LayerOptions', () => {
    +      const options: LayerOptions = {
    +        sensitive: true,
    +        end: false,
    +        strict: true
    +      };
    +
    +      const normalized = normalizeLayerOptionsToPathToRegexp(options);
    +
    +      assert.strictEqual(normalized.sensitive, true);
    +      assert.strictEqual(normalized.end, false);
    +      assert.strictEqual('strict' in normalized, true);
    +      assert.strictEqual(normalized.strict, true);
    +    });
    +
    +    it('should convert strict to trailing when trailing is not provided', () => {
    +      const options1: LayerOptions = { strict: true };
    +      const normalized1 = normalizeLayerOptionsToPathToRegexp(options1);
    +      assert.strictEqual('strict' in normalized1, true);
    +      assert.strictEqual(normalized1.strict, true);
    +
    +      const options2: LayerOptions = { strict: false };
    +      const normalized2 = normalizeLayerOptionsToPathToRegexp(options2);
    +      assert.strictEqual('strict' in normalized2, true);
    +      assert.strictEqual(normalized2.strict, false);
    +    });
    +
    +    it('should preserve trailing when both strict and trailing are provided', () => {
    +      const options: LayerOptions = {
    +        strict: true,
    +        trailing: true
    +      };
    +
    +      const normalized = normalizeLayerOptionsToPathToRegexp(options);
    +      assert.strictEqual(normalized.trailing, true);
    +    });
    +
    +    it('should remove undefined values', () => {
    +      const options: LayerOptions = {
    +        sensitive: undefined,
    +        end: true,
    +        strict: undefined
    +      };
    +
    +      const normalized = normalizeLayerOptionsToPathToRegexp(options);
    +      assert.strictEqual('sensitive' in normalized, false);
    +      assert.strictEqual(normalized.end, true);
    +      assert.strictEqual('strict' in normalized, false);
    +    });
    +
    +    it('should handle empty options object', () => {
    +      const normalized = normalizeLayerOptionsToPathToRegexp({});
    +
    +      assert.strictEqual(typeof normalized, 'object');
    +      assert.strictEqual(Object.keys(normalized).length, 0);
    +    });
    +
    +    it('should handle all LayerOptions properties', () => {
    +      const options: LayerOptions = {
    +        sensitive: true,
    +        strict: false,
    +        trailing: true,
    +        end: true,
    +        name: 'test-route',
    +        prefix: '/api',
    +        ignoreCaptures: true,
    +        pathAsRegExp: false
    +      };
    +
    +      const normalized = normalizeLayerOptionsToPathToRegexp(options);
    +
    +      assert.strictEqual(normalized.sensitive, true);
    +      assert.strictEqual(normalized.trailing, true);
    +      assert.strictEqual(normalized.end, true);
    +      assert.strictEqual('name' in normalized, false);
    +      assert.strictEqual('prefix' in normalized, false);
    +      assert.strictEqual('ignoreCaptures' in normalized, false);
    +      assert.strictEqual('pathAsRegExp' in normalized, false);
    +    });
    +
    +    it('should handle undefined options', () => {
    +      const normalized = normalizeLayerOptionsToPathToRegexp(undefined as any);
    +
    +      assert.strictEqual(typeof normalized, 'object');
    +    });
    +  });
    +
    +  describe('integration tests', () => {
    +    it('should work together: compilePathToRegexp + compilePath', () => {
    +      const path = '/users/:id';
    +
    +      const { regexp, keys } = compilePathToRegexp(path);
    +      assert.strictEqual(keys.length, 1);
    +
    +      const urlGenerator = compilePath(path);
    +      const url = urlGenerator({ id: '123' });
    +
    +      assert.strictEqual(regexp.test(url), true);
    +    });
    +
    +    it('should work together: parsePath + compilePath', () => {
    +      const path = '/users/:userId/posts/:postId';
    +
    +      const parseResult = parsePath(path);
    +      const tokens = Array.isArray(parseResult)
    +        ? parseResult
    +        : (parseResult as any).tokens || [];
    +      const paramNames = tokens
    +        .filter(
    +          (token: any) => token && typeof token === 'object' && 'name' in token
    +        )
    +        .map((token: any) => token.name);
    +
    +      assert.strictEqual(paramNames.includes('userId'), true);
    +      assert.strictEqual(paramNames.includes('postId'), true);
    +
    +      const urlGenerator = compilePath(path);
    +      const url = urlGenerator({ userId: '123', postId: '456' });
    +
    +      assert.strictEqual(url, '/users/123/posts/456');
    +    });
    +
    +    it('should handle complex path with all options', () => {
    +      const options: LayerOptions = {
    +        sensitive: true,
    +        strict: false,
    +        end: true
    +      };
    +
    +      const path = '/Users/:Id';
    +      const { regexp, keys } = compilePathToRegexp(path, options);
    +
    +      assert.strictEqual(keys.length, 1);
    +      assert.strictEqual(regexp.test('/Users/123'), true);
    +      assert.strictEqual(regexp.test('/users/123'), false);
    +    });
    +  });
    +});
    
  • tsconfig.bench.json+20 0 added
    @@ -0,0 +1,20 @@
    +{
    +  "extends": "./tsconfig.json",
    +  "compilerOptions": {
    +    "module": "CommonJS",
    +    "moduleResolution": "node",
    +    "esModuleInterop": true,
    +    "allowSyntheticDefaultImports": true
    +  },
    +  "include": ["bench/**/*.ts"],
    +  "ts-node": {
    +    "transpileOnly": true,
    +    "files": true,
    +    "compilerOptions": {
    +      "module": "CommonJS",
    +      "moduleResolution": "node",
    +      "esModuleInterop": true,
    +      "allowSyntheticDefaultImports": true
    +    }
    +  }
    +}
    
  • tsconfig.json+51 0 added
    @@ -0,0 +1,51 @@
    +{
    +  "compilerOptions": {
    +    "target": "ES2022",
    +    "module": "nodenext",
    +    "lib": ["ES2023"],
    +    "moduleResolution": "nodenext",
    +    "outDir": "./dist",
    +    "rootDir": "./src",
    +    "declaration": true,
    +    "declarationMap": true,
    +    "sourceMap": true,
    +    "removeComments": false,
    +    "esModuleInterop": true,
    +    "allowSyntheticDefaultImports": true,
    +    "resolveJsonModule": true,
    +    "isolatedModules": true,
    +    "strict": true,
    +    "noImplicitAny": true,
    +    "strictNullChecks": true,
    +    "strictFunctionTypes": true,
    +    "strictBindCallApply": true,
    +    "strictPropertyInitialization": true,
    +    "noImplicitThis": true,
    +    "alwaysStrict": true,
    +    "noUnusedLocals": true,
    +    "noUnusedParameters": true,
    +    "noImplicitReturns": true,
    +    "noFallthroughCasesInSwitch": true,
    +    "noUncheckedIndexedAccess": false,
    +    "noImplicitOverride": true,
    +    "noPropertyAccessFromIndexSignature": false,
    +    "skipLibCheck": true,
    +    "forceConsistentCasingInFileNames": true,
    +    "experimentalDecorators": true,
    +    "emitDecoratorMetadata": true,
    +    "baseUrl": ".",
    +    "paths": {
    +      "@/*": ["src/*"]
    +    },
    +    "types": ["node"]
    +  },
    +  "include": ["src/**/*.ts"],
    +  "exclude": [
    +    "node_modules",
    +    "dist",
    +    "test",
    +    "recipes/**/*.ts",
    +    "**/*.test.ts",
    +    "**/*.spec.ts"
    +  ]
    +}
    
  • tsconfig.recipes.json+23 0 added
    @@ -0,0 +1,23 @@
    +{
    +  "extends": "./tsconfig.json",
    +  "compilerOptions": {
    +    "module": "CommonJS",
    +    "moduleResolution": "node",
    +    "esModuleInterop": true,
    +    "allowSyntheticDefaultImports": true,
    +    "skipLibCheck": true,
    +    "rootDir": "."
    +  },
    +  "include": ["recipes/**/*.ts"],
    +  "exclude": ["node_modules", "dist", "test"],
    +  "ts-node": {
    +    "transpileOnly": true,
    +    "files": true,
    +    "compilerOptions": {
    +      "module": "CommonJS",
    +      "moduleResolution": "node",
    +      "esModuleInterop": true,
    +      "allowSyntheticDefaultImports": true
    +    }
    +  }
    +}
    
  • tsconfig.ts-node.json+19 0 added
    @@ -0,0 +1,19 @@
    +{
    +  "extends": "./tsconfig.json",
    +  "compilerOptions": {
    +    "module": "NodeNext",
    +    "moduleResolution": "NodeNext",
    +    "noImplicitAny": false,
    +    "noUnusedLocals": false,
    +    "noUnusedParameters": false,
    +    "strictNullChecks": false,
    +    "strictPropertyInitialization": false,
    +    "strictFunctionTypes": false
    +  },
    +  "ts-node": {
    +    "transpileOnly": true,
    +    "files": true,
    +    "include": ["recipes/**/*.ts", "src/**/*.ts"]
    +  },
    +  "include": ["recipes/**/*.ts", "src/**/*.ts"]
    +}
    
  • tsconfig.typecheck.json+15 0 added
    @@ -0,0 +1,15 @@
    +{
    +  "extends": "./tsconfig.json",
    +  "compilerOptions": {
    +    "noEmit": true,
    +    "skipLibCheck": true,
    +    "rootDir": "."
    +  },
    +  "include": [
    +    "src/**/*.ts",
    +    "recipes/**/*.ts",
    +    "bench/**/*.ts",
    +    "test/**/*.ts"
    +  ],
    +  "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
    +}
    
  • tsup.config.ts+15 0 added
    @@ -0,0 +1,15 @@
    +import { defineConfig } from 'tsup';
    +
    +const tsupConfig = defineConfig({
    +  name: '@koa/router',
    +  entry: ['src/*.ts'],
    +  target: 'esnext',
    +  format: ['cjs', 'esm'],
    +  dts: true,
    +  splitting: false,
    +  sourcemap: false,
    +  clean: true,
    +  platform: 'node'
    +});
    +
    +export default tsupConfig;
    
  • yarn.lock+3423 0 added

Vulnerability mechanics

Root cause

"Middleware registered via router.use() is silently dropped from the execution chain when the router prefix contains path parameters."

Attack vector

An attacker sends an HTTP request to a route that is protected by middleware (e.g., authentication, authorization, rate-limiting, or input sanitization) that was registered via `router.use()` on a router with a parameterized prefix. Because the middleware is silently skipped when the prefix contains path parameters, the request reaches the route handler without passing through the intended middleware. The attacker does not need authentication or any special privileges — the CVSS vector is AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L [ref_id=1].

Affected code

The vulnerability resides in the `@koa/router` package, versions 14.0.0 to before 15.0.0. The defect is in the router's prefix-handling logic: when a router prefix contains path parameters (e.g., `/things/:thing_id`), middleware registered via `router.use()` is silently dropped from the execution chain for routes under that prefix. The patch is a complete TypeScript rewrite of the router library [patch_id=2539779].

What the fix does

The fix is a comprehensive TypeScript rewrite of the entire `@koa/router` library, published as version 15.0.0 [patch_id=2539779]. The commit message states it "fix[es] all reported bugs" and "add[s] all effective enhancements." The patch includes new tests that verify middleware is correctly executed when router prefixes contain path parameters (e.g., the test "nested router middleware has access to parent path parameters" and the test "should allow setting a prefix" with `/things/:thing_id`). The rewrite ensures that middleware registered via `router.use()` is no longer silently dropped from the execution chain when the router prefix contains path parameters.

Preconditions

  • configThe application uses @koa/router version 14.0.0 up to (but not including) 15.0.0
  • configA router is configured with a prefix that contains path parameters (e.g., { prefix: '/things/:thing_id' })
  • configMiddleware (such as authentication, authorization, rate limiting, or input sanitization) is registered via router.use() on that router
  • networkThe attacker sends an HTTP request to a route under that parameterized prefix

Generated on May 26, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.