From c47820d9c6d1dcf31dfe6f8440a7521893504c1c Mon Sep 17 00:00:00 2001
From: fengmk2 <suqian.yf@antgroup.com>
Date: Sat, 8 Jun 2024 23:51:11 +0800
Subject: [PATCH 01/13] feat: support cjs and esm both

BREAKING CHANGE: drop Node.js < 18.7.0 support

- Drop generator function support
- Drop Node.js < 18.7.0 support
---
 .eslintrc                                 |   5 +-
 .github/workflows/nodejs.yml              |   2 +-
 History.md => CHANGELOG.md                |   0
 README.md                                 |  34 +++++-
 lib/utils.js                              |  18 ---
 package.json                              |  84 +++++++++-----
 {lib => src}/egg_router.js                |   0
 index.js => src/index.js                  |   0
 lib/layer.js => src/layer.ts              | 134 ++++++++++++++--------
 {lib => src}/router.js                    |   0
 src/types.ts                              |   4 +
 src/utils.ts                              |  19 +++
 test/lib/{layer.test.js => layer.test.ts} |  19 ++-
 tsconfig.json                             |  10 ++
 14 files changed, 219 insertions(+), 110 deletions(-)
 rename History.md => CHANGELOG.md (100%)
 delete mode 100644 lib/utils.js
 rename {lib => src}/egg_router.js (100%)
 rename index.js => src/index.js (100%)
 rename lib/layer.js => src/layer.ts (59%)
 rename {lib => src}/router.js (100%)
 create mode 100644 src/types.ts
 create mode 100644 src/utils.ts
 rename test/lib/{layer.test.js => layer.test.ts} (96%)
 create mode 100644 tsconfig.json

diff --git a/.eslintrc b/.eslintrc
index c799fe5..9bcdb46 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,3 +1,6 @@
 {
-  "extends": "eslint-config-egg"
+  "extends": [
+    "eslint-config-egg/typescript",
+    "eslint-config-egg/lib/rules/enforce-node-prefix"
+  ]
 }
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index a59ba46..5a2a289 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -12,6 +12,6 @@ jobs:
     uses: node-modules/github-actions/.github/workflows/node-test.yml@master
     with:
       os: 'ubuntu-latest'
-      version: '8, 10, 12, 14, 16, 18, 20, 22'
+      version: '18, 20, 22'
     secrets:
       CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/History.md b/CHANGELOG.md
similarity index 100%
rename from History.md
rename to CHANGELOG.md
diff --git a/README.md b/README.md
index 3530802..7595ad4 100644
--- a/README.md
+++ b/README.md
@@ -27,20 +27,22 @@ Router core component for [Egg.js](https://github.com/eggjs).
       - [router.param(param, middleware) ⇒ Router](#routerparamparam-middleware--router)
       - [Router.url(path, params \[, options\]) ⇒ String](#routerurlpath-params--options--string)
   - [Tests](#tests)
+  - [Breaking changes on v3](#breaking-changes-on-v3)
   - [License](#license)
 
 <a name="exp_module_egg-router--Router"></a>
 
 ### Router ⏏
+
 **Kind**: Exported class
 <a name="new_module_egg-router--Router_new"></a>
 
 #### new Router([opts])
-Create a new router.
 
+Create a new router.
 
 | Param | Type | Description |
-| --- | --- | --- |
+| ---   | ---   | --- |
 | [opts] | <code>Object</code> |  |
 | [opts.prefix] | <code>String</code> | prefix router paths |
 
@@ -62,9 +64,11 @@ app
   .use(router.routes())
   .use(router.allowedMethods());
 ```
+
 <a name="module_egg-router--Router+get|put|post|patch|delete|del"></a>
 
 #### 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()`.
 
@@ -180,7 +184,7 @@ used to convert paths to regular expressions.
 **Kind**: instance property of <code>[Router](#exp_module_egg-router--Router)</code>
 
 | Param | Type | Description |
-| --- | --- | --- |
+| ---   | ---  | --- |
 | path | <code>String</code> |  |
 | [middleware] | <code>function</code> | route middleware(s) |
 | callback | <code>function</code> | route callback |
@@ -194,6 +198,7 @@ Returns router middleware which dispatches a route matching the request.
 <a name="module_egg-router--Router+use"></a>
 
 #### router.use([path], middleware) ⇒ <code>Router</code>
+
 Use given middleware.
 
 Middleware run in the order they are defined by `.use()`. They are invoked
@@ -209,6 +214,7 @@ sequentially, requests start at the first middleware and work their way
 | [...] | <code>function</code> |
 
 **Example**
+
 ```javascript
 // session middleware will run before authorize
 router
@@ -223,9 +229,11 @@ router.use(['/users', '/admin'], userAuth());
 
 app.use(router.routes());
 ```
+
 <a name="module_egg-router--Router+prefix"></a>
 
 #### 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_egg-router--Router)</code>
@@ -235,12 +243,15 @@ Set the path prefix for a Router instance that was already initialized.
 | prefix | <code>String</code> |
 
 **Example**
+
 ```javascript
 router.prefix('/things/:thing_id')
 ```
+
 <a name="module_egg-router--Router+allowedMethods"></a>
 
 #### 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.
@@ -255,6 +266,7 @@ with `405 Method Not Allowed` and `501 Not Implemented` as appropriate.
 | [options.methodNotAllowed] | <code>function</code> | throw the returned value in place of the default MethodNotAllowed error |
 
 **Example**
+
 ```javascript
 var Koa = require('koa');
 var Router = require('egg-router');
@@ -283,9 +295,11 @@ app.use(router.allowedMethods({
   methodNotAllowed: () => new Boom.methodNotAllowed()
 }));
 ```
+
 <a name="module_egg-router--Router+redirect"></a>
 
 #### 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.
@@ -314,6 +328,7 @@ router.all('/login', ctx => {
 <a name="module_egg-router--Router+route"></a>
 
 #### router.route(name) ⇒ <code>Layer</code> &#124; <code>false</code>
+
 Lookup route with given `name`.
 
 **Kind**: instance method of <code>[Router](#exp_module_egg-router--Router)</code>
@@ -325,6 +340,7 @@ Lookup route with given `name`.
 <a name="module_egg-router--Router+url"></a>
 
 #### router.url(name, params, [options]) ⇒ <code>String</code> &#124; <code>Error</code>
+
 Generate URL for route. Takes a route name and map of named `params`.
 
 **Kind**: instance method of <code>[Router](#exp_module_egg-router--Router)</code>
@@ -337,6 +353,7 @@ Generate URL for route. Takes a route name and map of named `params`.
 | [options.query] | <code>Object</code> &#124; <code>String</code> | query options |
 
 **Example**
+
 ```javascript
 router.get('user', '/users/:id', (ctx, next) => {
   // ...
@@ -359,9 +376,11 @@ router.url('user', { id: 3 }, { query: { limit: 1 } });
 router.url('user', { id: 3 }, { query: "limit=1" });
 // => "/users/3?limit=1"
 ```
+
 <a name="module_egg-router--Router+param"></a>
 
 #### router.param(param, middleware) ⇒ <code>Router</code>
+
 Run middleware for named route parameters. Useful for auto-loading or
 validation.
 
@@ -373,6 +392,7 @@ validation.
 | middleware | <code>function</code> |
 
 **Example**
+
 ```javascript
 router
   .param('user', (id, ctx, next) => {
@@ -391,9 +411,11 @@ router
   // /users/3 => {"id": 3, "name": "Alex"}
   // /users/3/friends => [{"id": 4, "name": "TJ"}]
 ```
+
 <a name="module_egg-router--Router.url"></a>
 
 #### Router.url(path, params [, options]) ⇒ <code>String</code>
+
 Generate URL from url pattern and given `params`.
 
 **Kind**: static method of <code>[Router](#exp_module_egg-router--Router)</code>
@@ -406,6 +428,7 @@ Generate URL from url pattern and given `params`.
 | [options.query] | <code>Object</code> &#124; <code>String</code> | query options |
 
 **Example**
+
 ```javascript
 var url = Router.url('/users/:id', {id: 1});
 // => "/users/1"
@@ -418,6 +441,11 @@ const url = Router.url('/users/:id', {id: 1}, {query: { active: true }});
 
 Run tests using `npm test`.
 
+## Breaking changes on v3
+
+- Drop generator function support
+- Drop Node.js < 18.7.0 support
+
 ## License
 
 [MIT](LICENSE)
diff --git a/lib/utils.js b/lib/utils.js
deleted file mode 100644
index f28c546..0000000
--- a/lib/utils.js
+++ /dev/null
@@ -1,18 +0,0 @@
-'use strict';
-
-const convert = require('koa-convert');
-const is = require('is-type-of');
-const co = require('co');
-
-module.exports = {
-  async callFn(fn, args, ctx) {
-    args = args || [];
-    if (!is.function(fn)) return;
-    if (is.generatorFunction(fn)) fn = co.wrap(fn);
-    return ctx ? fn.call(ctx, ...args) : fn(...args);
-  },
-
-  middleware(fn) {
-    return is.generatorFunction(fn) ? convert(fn) : fn;
-  },
-};
diff --git a/package.json b/package.json
index 9ff3c12..19b3977 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,12 @@
 {
   "name": "@eggjs/router",
   "version": "2.0.1",
+  "engines": {
+    "node": ">= 18.7.0"
+  },
+  "publishConfig": {
+    "access": "public"
+  },
   "description": "Router middleware for egg/koa. Provides RESTful resource routing.",
   "repository": {
     "type": "git",
@@ -9,10 +15,6 @@
   "bugs": {
     "url": "https://github.com/eggjs/egg/issues"
   },
-  "files": [
-    "lib",
-    "index.js"
-  ],
   "author": "eggjs",
   "keywords": [
     "koa",
@@ -21,36 +23,62 @@
     "route"
   ],
   "dependencies": {
-    "co": "^4.6.0",
-    "debug": "^3.1.0",
-    "http-errors": "^1.3.1",
-    "inflection": "^1.12.0",
-    "is-type-of": "^1.2.1",
-    "koa-compose": "^3.0.0",
-    "koa-convert": "^1.2.0",
-    "methods": "^1.0.1",
+    "http-errors": "^2.0.0",
+    "inflection": "^3.0.0",
+    "is-type-of": "^2.1.0",
+    "koa-compose": "^4.1.0",
+    "methods": "^1.1.2",
     "path-to-regexp": "^1.1.1",
-    "urijs": "^1.19.0",
-    "utility": "^1.15.0"
+    "urijs": "^1.19.11",
+    "utility": "^2.1.0"
   },
   "devDependencies": {
-    "egg-bin": "^4.10.0",
-    "eslint": "^5.13.0",
-    "eslint-config-egg": "^7.1.0",
-    "expect.js": "^0.3.1",
-    "koa": "^2.7.0",
-    "mocha": "^2.0.1",
-    "should": "^6.0.3",
-    "supertest": "^1.0.1"
+    "@eggjs/koa": "^2.18.0",
+    "@types/mocha": "^10.0.6",
+    "egg-bin": "6",
+    "eslint": "8",
+    "eslint-config-egg": "13",
+    "git-contributor": "^2.1.5",
+    "supertest": "^1.0.1",
+    "tshy": "^1.15.1",
+    "tshy-after": "^1.0.0",
+    "typescript": "^5.4.5"
   },
   "scripts": {
+    "lint": "eslint src test --ext ts",
+    "pretest": "npm run lint -- --fix && npm run prepublishOnly",
+    "test": "egg-bin test",
     "test-local": "egg-bin test",
-    "test": "npm run lint && egg-bin test",
-    "ci": "npm run lint && egg-bin cov",
-    "lint": "eslint ."
+    "preci": "npm run lint && npm run prepublishOnly",
+    "ci": "egg-bin cov",
+    "contributor": "git-contributor",
+    "prepublishOnly": "tshy && tshy-after"
   },
-  "engines": {
-    "node": ">= 8.5.0"
+  "license": "MIT",
+  "type": "module",
+  "tshy": {
+    "exports": {
+      "./package.json": "./package.json",
+      ".": "./src/index.ts"
+    }
+  },
+  "exports": {
+    "./package.json": "./package.json",
+    ".": {
+      "import": {
+        "types": "./dist/esm/index.d.ts",
+        "default": "./dist/esm/index.js"
+      },
+      "require": {
+        "types": "./dist/commonjs/index.d.ts",
+        "default": "./dist/commonjs/index.js"
+      }
+    }
   },
-  "license": "MIT"
+  "files": [
+    "dist",
+    "src"
+  ],
+  "main": "./dist/commonjs/index.js",
+  "types": "./dist/commonjs/index.d.ts"
 }
diff --git a/lib/egg_router.js b/src/egg_router.js
similarity index 100%
rename from lib/egg_router.js
rename to src/egg_router.js
diff --git a/index.js b/src/index.js
similarity index 100%
rename from index.js
rename to src/index.js
diff --git a/lib/layer.js b/src/layer.ts
similarity index 59%
rename from lib/layer.js
rename to src/layer.ts
index 90cf992..fac14ad 100644
--- a/lib/layer.js
+++ b/src/layer.ts
@@ -1,49 +1,76 @@
-'use strict';
+import { debuglog } from 'node:util';
+import pathToRegExp, { type Key } from 'path-to-regexp';
+import URI from 'urijs';
+import { decodeURIComponent as safeDecodeURIComponent } from 'utility';
+import type {
+  MiddlewareFunc,
+  MiddlewareFuncWithParamProperty,
+  ParamMiddlewareFunc,
+} from './types.js';
+
+const debug = debuglog('egg-router:layer');
+
+export interface LayerOptions {
+  prefix?: string;
+  /** route name */
+  name?: string;
+  /** case sensitive (default: false) */
+  sensitive?: boolean;
+  /** require the trailing slash (default: false) */
+  strict?: boolean;
+  ignoreCaptures?: boolean;
+}
+
+export interface LayerURLOptions {
+  query?: string | object;
+}
+
+export class Layer {
+  readonly opts: LayerOptions;
+  readonly name?: string;
+  readonly methods: string[] = [];
+  readonly stack: MiddlewareFuncWithParamProperty[];
+  path: string;
+  regexp: RegExp;
+  paramNames: Key[] = [];
 
-const debug = require('debug')('egg-router:layer');
-const pathToRegExp = require('path-to-regexp');
-const uri = require('urijs');
-const utility = require('utility');
-
-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 {Array|Function} middlewares Layer callback/middleware or series of.
    * @param {Object=} opts optional params
    * @param {String=} opts.name route name
    * @param {String=} opts.sensitive case sensitive (default: false)
    * @param {String=} opts.strict require the trailing slash (default: false)
    * @private
    */
-  constructor(path, methods, middleware, opts) {
-    this.opts = opts || {};
-    this.name = this.opts.name || null;
-    this.methods = [];
-    this.paramNames = [];
-    this.stack = Array.isArray(middleware) ? middleware : [ middleware ];
-
-    methods.forEach(function(method) {
+  constructor(path: string | RegExp, methods: string[], middlewares: MiddlewareFunc | MiddlewareFunc[], opts?: LayerOptions) {
+    this.opts = opts ?? {};
+    this.opts.prefix = this.opts.prefix ?? '';
+    this.name = this.opts.name;
+    this.stack = Array.isArray(middlewares) ? middlewares : [ middlewares ];
+
+    for (const method of methods) {
       const l = this.methods.push(method.toUpperCase());
       if (this.methods[l - 1] === 'GET') {
         this.methods.unshift('HEAD');
       }
-    }, this);
+    }
 
     // ensure middleware is a function
-    this.stack.forEach(function(fn) {
+    this.stack.forEach(fn => {
       const type = (typeof fn);
       if (type !== 'function') {
-        throw new Error(
+        throw new TypeError(
           methods.toString() + ' `' + (this.opts.name || path) + '`: `middleware` '
-          + 'must be a function, not `' + type + '`'
+          + 'must be a function, not `' + type + '`',
         );
       }
-    }, this);
+    });
 
-    this.path = path;
+    this.path = typeof path === 'string' ? path : String(path);
     this.regexp = pathToRegExp(path, this.paramNames, this.opts);
 
     debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
@@ -56,7 +83,7 @@ module.exports = class Layer {
    * @return {Boolean} matched or not
    * @private
    */
-  match(path) {
+  match(path: string): boolean {
     return this.regexp.test(path);
   }
 
@@ -69,13 +96,14 @@ module.exports = class Layer {
    * @return {Object} params object
    * @private
    */
-  params(path, captures, existingParams) {
+  params(path: string, captures: Array<string>, existingParams?: Record<string, any>): object {
     const params = existingParams || {};
 
     for (let len = captures.length, i = 0; i < len; i++) {
-      if (this.paramNames[i]) {
+      const paramName = this.paramNames[i];
+      if (paramName) {
         const c = captures[i];
-        params[this.paramNames[i].name] = c ? utility.decodeURIComponent(c) : c;
+        params[paramName.name] = c ? safeDecodeURIComponent(c) : c;
       }
     }
     return params;
@@ -88,9 +116,10 @@ module.exports = class Layer {
    * @return {Array.<String>} captures strings
    * @private
    */
-  captures(path) {
+  captures(path: string): Array<string> {
     if (this.opts.ignoreCaptures) return [];
-    return path.match(this.regexp).slice(1);
+    const m = path.match(this.regexp);
+    return m ? m.slice(1) : [];
   }
 
   /**
@@ -109,43 +138,53 @@ module.exports = class Layer {
    * @return {String} url string
    * @private
    */
-  url(params, options) {
-    let args = params;
+  url(params: object, options?: LayerURLOptions): string {
+    let args: Array<string | number> | object = params;
     const url = this.path.replace(/\(\.\*\)/g, '');
     const toPath = pathToRegExp.compile(url);
 
     if (typeof params !== 'object') {
+      // route.url(123, 456, options);
       args = Array.prototype.slice.call(arguments);
-      if (typeof args[args.length - 1] === 'object') {
-        options = args[args.length - 1];
-        args = args.slice(0, args.length - 1);
+      if (Array.isArray(args)) {
+        if (typeof args[args.length - 1] === 'object') {
+          options = args[args.length - 1];
+          args = args.slice(0, args.length - 1);
+        }
       }
     }
 
     const tokens = pathToRegExp.parse(url);
-    let replace = {};
+    let replace: Record<string, any> = {};
 
-    if (args instanceof Array) {
+    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++];
+        const token = tokens[i];
+        if (typeof token === 'object' && token.name) {
+          replace[token.name] = args[j++];
+        }
       }
-    } else if (tokens.some(token => token.name)) {
+    } else if (tokens.some(token => typeof token === 'object' && token.name)) {
       replace = params;
     } else {
       options = params;
     }
 
-    let replaced = toPath(replace);
+    const replaced = toPath(replace);
 
-    if (options && options.query) {
-      replaced = new uri(replaced);
-      replaced.search(options.query);
-      return replaced.toString();
+    if (options?.query) {
+      const urlObject = new URI(replaced);
+      urlObject.search(options.query);
+      return urlObject.toString();
     }
 
     return replaced;
   }
 
+  #urlWithArgs(args: any[], options?: object) {
+
+  }
+
   /**
    * Run validations on route named parameters.
    *
@@ -168,15 +207,15 @@ module.exports = class Layer {
    * @return {Layer} layer instance
    * @private
    */
-  param(param, fn) {
+  param(param: string, fn: ParamMiddlewareFunc): Layer {
     const stack = this.stack;
     const params = this.paramNames;
-    const middleware = function(ctx, next) {
+    const middleware: MiddlewareFuncWithParamProperty = function(this: any, ctx, next) {
       return fn.call(this, ctx.params[param], ctx, next);
     };
     middleware.param = param;
 
-    const names = params.map(function(p) {
+    const names = params.map(p => {
       return p.name;
     });
 
@@ -205,13 +244,12 @@ module.exports = class Layer {
    * @return {Layer} layer instance
    * @private
    */
-  setPrefix(prefix) {
+  setPrefix(prefix: string): Layer {
     if (this.path) {
       this.path = prefix + this.path;
       this.paramNames = [];
       this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
     }
-
     return this;
   }
-};
+}
diff --git a/lib/router.js b/src/router.js
similarity index 100%
rename from lib/router.js
rename to src/router.js
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..5cb66f7
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,4 @@
+export type Next = () => Promise<void>;
+export type MiddlewareFunc = (ctx: any, next: Next) => Promise<void> | void;
+export type MiddlewareFuncWithParamProperty = MiddlewareFunc & { param?: string };
+export type ParamMiddlewareFunc = (param: string, ctx: any, next: Next) => Promise<void> | void;
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..b8bd1f2
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,19 @@
+import { isFunction, isGeneratorFunction } from 'is-type-of';
+
+export async function callFn(fn: Function, args: any[], ctx: unknown): Promise<unknown> {
+  args = args || [];
+  if (!isFunction(fn)) {
+    return;
+  }
+  if (isGeneratorFunction(fn)) {
+    throw new TypeError(`Please use async function instead of generator function: ${fn.toString()}`);
+  }
+  return ctx ? fn.call(ctx, ...args) : fn(...args);
+}
+
+export function middleware(fn: Function) {
+  if (isGeneratorFunction(fn)) {
+    throw new TypeError(`Please use async function instead of generator function: ${fn.toString()}`);
+  }
+  return fn;
+}
diff --git a/test/lib/layer.test.js b/test/lib/layer.test.ts
similarity index 96%
rename from test/lib/layer.test.js
rename to test/lib/layer.test.ts
index 536f9ec..c11ebe1 100644
--- a/test/lib/layer.test.js
+++ b/test/lib/layer.test.ts
@@ -1,15 +1,12 @@
-'use strict';
+import Application from '@eggjs/koa';
+import http from 'http';
+import request from 'supertest';
+import Router from '../../lib/router';
+import Layer from '../../lib/layer';
 
-const Koa = require('koa');
-const http = require('http');
-const request = require('supertest');
-require('should');
-const Router = require('../../lib/router');
-const Layer = require('../../lib/layer');
-
-describe('test/lib/layer.test.js', function() {
+describe('test/lib/layer.test.js', () => {
   it('composes multiple callbacks/middlware', function(done) {
-    const app = new Koa();
+    const app = new Application();
     const router = new Router();
     app.use(router.routes());
     router.get(
@@ -21,7 +18,7 @@ describe('test/lib/layer.test.js', function() {
       function(ctx, next) {
         ctx.status = 204;
         return next();
-      }
+      },
     );
     request(http.createServer(app.callback()))
       .get('/programming/how-to-node')
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..ff41b73
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,10 @@
+{
+  "extends": "@eggjs/tsconfig",
+  "compilerOptions": {
+    "strict": true,
+    "noImplicitAny": true,
+    "target": "ES2022",
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext"
+  }
+}

From 13c6fb5ed579d91952088e95c09dc4294e557a40 Mon Sep 17 00:00:00 2001
From: fengmk2 <suqian.yf@antgroup.com>
Date: Sat, 8 Jun 2024 23:53:26 +0800
Subject: [PATCH 02/13] f

---
 .github/workflows/nodejs.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 5a2a289..003b101 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -12,6 +12,6 @@ jobs:
     uses: node-modules/github-actions/.github/workflows/node-test.yml@master
     with:
       os: 'ubuntu-latest'
-      version: '18, 20, 22'
+      version: '18.7.0, 18, 20, 22'
     secrets:
       CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

From 351f2fccc73fabfa3822609f3eb46cad5984467b Mon Sep 17 00:00:00 2001
From: fengmk2 <suqian.yf@antgroup.com>
Date: Mon, 10 Jun 2024 21:03:59 +0800
Subject: [PATCH 03/13] f

---
 .gitignore             |    3 +
 package.json           |    7 +
 src/index.js           |   10 -
 src/index.ts           |    3 +
 src/layer.ts           |   48 +-
 src/router.js          |  724 -----------------------------
 src/router.ts          | 1002 ++++++++++++++++++++++++++++++++++++++++
 src/types.ts           |    1 +
 src/utils.ts           |    7 +-
 test/lib/layer.test.ts |  231 +++++----
 10 files changed, 1155 insertions(+), 881 deletions(-)
 delete mode 100644 src/index.js
 create mode 100644 src/index.ts
 delete mode 100644 src/router.js
 create mode 100644 src/router.ts

diff --git a/.gitignore b/.gitignore
index 6903f3f..3a99497 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,3 +42,6 @@ yarn.lock
 !.env.test
 
 .DS_Store
+
+.tshy*
+dist
diff --git a/package.json b/package.json
index 19b3977..1027993 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,12 @@
   },
   "devDependencies": {
     "@eggjs/koa": "^2.18.0",
+    "@eggjs/tsconfig": "^1.3.3",
+    "@types/koa-compose": "^3.2.8",
+    "@types/methods": "^1.1.4",
     "@types/mocha": "^10.0.6",
+    "@types/supertest": "^6.0.2",
+    "@types/urijs": "^1.19.25",
     "egg-bin": "6",
     "eslint": "8",
     "eslint-config-egg": "13",
@@ -66,10 +71,12 @@
     "./package.json": "./package.json",
     ".": {
       "import": {
+        "source": "./src/index.ts",
         "types": "./dist/esm/index.d.ts",
         "default": "./dist/esm/index.js"
       },
       "require": {
+        "source": "./src/index.ts",
         "types": "./dist/commonjs/index.d.ts",
         "default": "./dist/commonjs/index.js"
       }
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index 7a9f149..0000000
--- a/src/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-'use strict';
-
-const KoaRouter = require('./lib/router');
-const EggRouter = require('./lib/egg_router');
-
-// for compact
-module.exports = KoaRouter;
-module.exports.KoaRouter = KoaRouter;
-module.exports.EggRouter = EggRouter;
-
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..54a0bbe
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,3 @@
+export * from './router.js';
+// import EggRouter from './lib/egg_router';
+
diff --git a/src/layer.ts b/src/layer.ts
index fac14ad..8f936d0 100644
--- a/src/layer.ts
+++ b/src/layer.ts
@@ -19,6 +19,7 @@ export interface LayerOptions {
   /** require the trailing slash (default: false) */
   strict?: boolean;
   ignoreCaptures?: boolean;
+  end?: boolean;
 }
 
 export interface LayerURLOptions {
@@ -46,7 +47,12 @@ export class Layer {
    * @param {String=} opts.strict require the trailing slash (default: false)
    * @private
    */
-  constructor(path: string | RegExp, methods: string[], middlewares: MiddlewareFunc | MiddlewareFunc[], opts?: LayerOptions) {
+  constructor(path: string | RegExp, methods: string[], middlewares: MiddlewareFunc | MiddlewareFunc[],
+    opts?: LayerOptions | string) {
+    if (typeof opts === 'string') {
+      // new Layer(path, methods, middlewares, name);
+      opts = { name: opts };
+    }
     this.opts = opts ?? {};
     this.opts.prefix = this.opts.prefix ?? '';
     this.name = this.opts.name;
@@ -90,14 +96,14 @@ export class Layer {
   /**
    * Returns map of URL parameters for given `path` and `paramNames`.
    *
-   * @param {String} path path string
+   * @param {String} _path path string
    * @param {Array.<String>} captures captures strings
-   * @param {Object=} existingParams existing params
+   * @param {Object=} [existingParams] existing params
    * @return {Object} params object
    * @private
    */
-  params(path: string, captures: Array<string>, existingParams?: Record<string, any>): object {
-    const params = existingParams || {};
+  params(_path: string, captures: Array<string>, existingParams?: Record<string, string>): Record<string, string> {
+    const params = existingParams ?? {};
 
     for (let len = captures.length, i = 0; i < len; i++) {
       const paramName = this.paramNames[i];
@@ -130,26 +136,30 @@ export class Layer {
    * ```javascript
    * var route = new Layer(['GET'], '/users/:id', fn);
    *
+   * route.url(123); // => "/users/123"
+   * route.url('123'); // => "/users/123"
    * route.url({ id: 123 }); // => "/users/123"
    * ```
    *
    * @param {Object} params url parameters
-   * @param {Object} [options] optional parameters
+   * @param {Object} paramsOrOptions optional parameters
    * @return {String} url string
    * @private
    */
-  url(params: object, options?: LayerURLOptions): string {
-    let args: Array<string | number> | object = params;
+  url(params?: string | number | object, ...paramsOrOptions: (string | number | object | LayerURLOptions)[]): string {
+    let args: Array<string | number | object> | object = params as object;
     const url = this.path.replace(/\(\.\*\)/g, '');
     const toPath = pathToRegExp.compile(url);
+    let options: LayerURLOptions | undefined;
 
-    if (typeof params !== 'object') {
-      // route.url(123, 456, options);
-      args = Array.prototype.slice.call(arguments);
+    if (params !== undefined && typeof params !== 'object') {
+      args = [ params, ...paramsOrOptions ];
+      // route.url(params1, params2, ..., options);
       if (Array.isArray(args)) {
-        if (typeof args[args.length - 1] === 'object') {
-          options = args[args.length - 1];
-          args = args.slice(0, args.length - 1);
+        const lastIndex = args.length - 1;
+        if (typeof args[lastIndex] === 'object') {
+          options = paramsOrOptions[lastIndex] as LayerURLOptions;
+          args = paramsOrOptions.slice(0, lastIndex);
         }
       }
     }
@@ -165,9 +175,11 @@ export class Layer {
         }
       }
     } else if (tokens.some(token => typeof token === 'object' && token.name)) {
-      replace = params;
+      // route.url(params);
+      replace = params as object;
     } else {
-      options = params;
+      // route.url(options);
+      options = params as LayerURLOptions;
     }
 
     const replaced = toPath(replace);
@@ -181,10 +193,6 @@ export class Layer {
     return replaced;
   }
 
-  #urlWithArgs(args: any[], options?: object) {
-
-  }
-
   /**
    * Run validations on route named parameters.
    *
diff --git a/src/router.js b/src/router.js
deleted file mode 100644
index e101a8a..0000000
--- a/src/router.js
+++ /dev/null
@@ -1,724 +0,0 @@
-'use strict';
-
-/**
- * RESTful resource routing middleware for eggjs.
- */
-
-const debug = require('debug')('egg-router');
-const compose = require('koa-compose');
-const HttpError = require('http-errors');
-const methods = require('methods');
-const Layer = require('./layer');
-
-/**
- * @module koa-router
- */
-class Router {
-  /**
-   * Create a new router.
-   *
-   * @example
-   *
-   * Basic usage:
-   *
-   * ```javascript
-   * var Koa = require('koa');
-   * var Router = require('koa-router');
-   *
-   * var app = new Koa();
-   * var router = new Router();
-   *
-   * router.get('/', (ctx, next) => {
-   *   // ctx.router available
-   * });
-   *
-   * app
-   *   .use(router.routes())
-   *   .use(router.allowedMethods());
-   * ```
-   *
-   * @alias module:koa-router
-   * @param {Object=} opts optional
-   * @param {String=} opts.prefix prefix router paths
-   * @class
-   */
-  constructor(opts) {
-    this.opts = opts || {};
-    this.methods = this.opts.methods || [
-      'HEAD',
-      'OPTIONS',
-      'GET',
-      'PUT',
-      'PATCH',
-      'POST',
-      'DELETE',
-    ];
-
-    this.params = {};
-    this.stack = [];
-  }
-
-  /**
-   * 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 path string
-   * @param {Function} middleware middleware function
-   * @return {Router} router instance
-   */
-  use(/* path, middleware */) {
-    const router = this;
-    const middleware = Array.prototype.slice.call(arguments);
-    let path;
-
-    // support array of paths
-    if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
-      middleware[0].forEach(function(p) {
-        router.use.apply(router, [ p ].concat(middleware.slice(1)));
-      });
-
-      return this;
-    }
-
-    const hasPath = typeof middleware[0] === 'string';
-    if (hasPath) {
-      path = middleware.shift();
-    }
-
-    middleware.forEach(function(m) {
-      if (m.router) {
-        m.router.stack.forEach(function(nestedLayer) {
-          if (path) nestedLayer.setPrefix(path);
-          if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix);
-          router.stack.push(nestedLayer);
-        });
-
-        if (router.params) {
-          Object.keys(router.params).forEach(function(key) {
-            m.router.param(key, router.params[key]);
-          });
-        }
-      } else {
-        router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
-      }
-    });
-
-    return this;
-  }
-
-  /**
-   * Set the path prefix for a Router instance that was already initialized.
-   *
-   * @example
-   *
-   * ```javascript
-   * router.prefix('/things/:thing_id')
-   * ```
-   *
-   * @param {String} prefix prefix string
-   * @return {Router} router instance
-   */
-  prefix(prefix) {
-    prefix = prefix.replace(/\/$/, '');
-
-    this.opts.prefix = prefix;
-
-    this.stack.forEach(function(route) {
-      route.setPrefix(prefix);
-    });
-
-    return this;
-  }
-
-  /**
-   * Returns router middleware which dispatches a route matching the request.
-   *
-   * @return {Function} middleware function
-   */
-  routes() {
-    const router = this;
-
-    const dispatch = function dispatch(ctx, next) {
-      debug('%s %s', ctx.method, ctx.path);
-
-      const path = router.opts.routerPath || ctx.routerPath || ctx.path;
-      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 layerChain = matchedLayers.reduce(function(memo, layer) {
-        memo.push(function(ctx, next) {
-          ctx.captures = layer.captures(path, ctx.captures);
-          ctx.params = layer.params(path, ctx.captures, ctx.params);
-          // ctx._matchedRouteName & ctx._matchedRoute for compatibility
-          ctx._matchedRouteName = ctx.routerName = layer.name;
-          if (!layer.name) ctx._matchedRouteName = undefined;
-          ctx._matchedRoute = ctx.routerPath = layer.path;
-          return next();
-        });
-        return memo.concat(layer.stack);
-      }, []);
-
-      return compose(layerChain)(ctx, next);
-    };
-
-    dispatch.router = this;
-
-    return dispatch;
-  }
-
-  /**
-   * 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
-   * var Koa = require('koa');
-   * var Router = require('koa-router');
-   *
-   * var app = new Koa();
-   * var router = new Router();
-   *
-   * app.use(router.routes());
-   * app.use(router.allowedMethods());
-   * ```
-   *
-   * **Example with [Boom](https://github.com/hapijs/boom)**
-   *
-   * ```javascript
-   * var Koa = require('koa');
-   * var Router = require('koa-router');
-   * var Boom = require('boom');
-   *
-   * var app = new Koa();
-   * var router = new Router();
-   *
-   * app.use(router.routes());
-   * app.use(router.allowedMethods({
-   *   throw: true,
-   *   notImplemented: () => new Boom.notImplemented(),
-   *   methodNotAllowed: () => new Boom.methodNotAllowed()
-   * }));
-   * ```
-   *
-   * @param {Object=} options optional params
-   * @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
-   * @return {Function} middleware function
-   */
-  allowedMethods(options) {
-    options = options || {};
-    const implemented = this.methods;
-
-    return function allowedMethods(ctx, next) {
-      return next().then(function() {
-        const allowed = {};
-
-        if (!ctx.status || ctx.status === 404) {
-          ctx.matched.forEach(function(route) {
-            route.methods.forEach(function(method) {
-              allowed[method] = method;
-            });
-          });
-
-          const allowedArr = Object.keys(allowed);
-
-          if (!implemented.includes(ctx.method)) {
-            if (options.throw) {
-              let notImplementedThrowable;
-              if (typeof options.notImplemented === 'function') {
-                notImplementedThrowable = options.notImplemented(); // set whatever the user returns from their function
-              } else {
-                notImplementedThrowable = new HttpError.NotImplemented();
-              }
-              throw notImplementedThrowable;
-            } else {
-              ctx.status = 501;
-              ctx.set('Allow', allowedArr.join(', '));
-            }
-          } else if (allowedArr.length) {
-            if (ctx.method === 'OPTIONS') {
-              ctx.status = 200;
-              ctx.body = '';
-              ctx.set('Allow', allowedArr.join(', '));
-            } else if (!allowed[ctx.method]) {
-              if (options.throw) {
-                let notAllowedThrowable;
-                if (typeof options.methodNotAllowed === 'function') {
-                  notAllowedThrowable = options.methodNotAllowed(); // set whatever the user returns from their function
-                } else {
-                  notAllowedThrowable = 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 path string
-   * @param {Function=} middleware You may also pass multiple middleware.
-   * @param {Function} callback callback function
-   * @return {Router} router instance
-   * @private
-   */
-  all(name, path/* , middleware */) {
-    let middleware;
-
-    if (typeof path === 'string') {
-      middleware = Array.prototype.slice.call(arguments, 2);
-    } else {
-      middleware = Array.prototype.slice.call(arguments, 1);
-      path = name;
-      name = null;
-    }
-
-    this.register(path, methods, middleware, {
-      name,
-    });
-
-    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).
-   * @return {Router} router instance
-   */
-  redirect(source, destination, code) {
-    // lookup source route by name
-    if (source[0] !== '/') {
-      source = this.url(source);
-    }
-
-    // lookup destination route by name
-    if (destination[0] !== '/') {
-      destination = this.url(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.
-   * @param {Object} [opts] optional params
-   * @return {Layer} layer instance
-   * @private
-   */
-  register(path, methods, middleware, opts) {
-    opts = opts || {};
-
-    const router = this;
-    const stack = this.stack;
-
-    // support array of paths
-    if (Array.isArray(path)) {
-      path.forEach(function(p) {
-        router.register.call(router, p, 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 || this.opts.sensitive || false,
-      strict: opts.strict || this.opts.strict || false,
-      prefix: opts.prefix || this.opts.prefix || '',
-      ignoreCaptures: opts.ignoreCaptures,
-    });
-
-    if (this.opts.prefix) {
-      route.setPrefix(this.opts.prefix);
-    }
-
-    // add parameter middleware
-    Object.keys(this.params).forEach(function(param) {
-      route.param(param, this.params[param]);
-    }, this);
-
-    stack.push(route);
-
-    return route;
-  }
-
-  /**
-   * Lookup route with given `name`.
-   *
-   * @param {String} name route name
-   * @return {Layer|false} layer instance of 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
-   * @return {String|Error} string or error instance
-   */
-  url(name/* , params */) {
-    const route = this.route(name);
-
-    if (route) {
-      const args = Array.prototype.slice.call(arguments, 1);
-      return route.url.apply(route, args);
-    }
-
-    return new Error('No route found for name: ' + name);
-  }
-
-  /**
-   * Match given `path` and return corresponding routes.
-   *
-   * @param {String} path path string
-   * @param {String} method method name
-   * @return {Object.<path, pathAndMethod>} returns layers that matched path and
-   * path and method.
-   * @private
-   */
-  match(path, method) {
-    const layers = this.stack;
-    let layer;
-    const matched = {
-      // matched path
-      path: [],
-      // matched path and method(including none method)
-      pathAndMethod: [],
-      // method matched or not
-      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) matched.route = true;
-        }
-        // if (layer.methods.length === 0) {
-        //   matched.pathAndMethod.push(layer);
-        // } else if (layer.methods.includes(method)) {
-        //   matched.pathAndMethod.push(layer);
-        //   matched.route = true;
-        // }
-      }
-    }
-
-    return matched;
-  }
-
-  /**
-   * 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
-   * @param {Function} middleware route middleware
-   * @return {Router} instance
-   */
-  param(param, middleware) {
-    this.params[param] = middleware;
-    this.stack.forEach(function(route) {
-      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()`.
- *
- * Additionaly, `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
- * var forums = new Router();
- * var 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
- * var 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.
- *
- * @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}
- */
-
-methods.forEach(function(method) {
-  Router.prototype[method] = function(name, path /* , middleware */) {
-    let 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;
-    }
-
-    this.register(path, [ method ], middleware, {
-      name,
-    });
-
-    return this;
-  };
-});
-
-// Alias for `router.delete()` because delete is a reserved word
-Router.prototype.del = Router.prototype.delete;
-
-/**
- * Generate URL from url pattern and given `params`.
- *
- * @example
- *
- * ```javascript
- * var url = Router.url('/users/:id', {id: 1});
- * // => "/users/1"
- * ```
- *
- * @param {String} path url pattern
- * @param {Object} params url parameters
- * @return {String} url string
- */
-Router.url = function(path/* , params */) {
-  const args = Array.prototype.slice.call(arguments, 1);
-  return Layer.prototype.url.apply({ path }, args);
-};
-
-Router.prototype.middleware = Router.prototype.routes;
-
-module.exports = Router;
diff --git a/src/router.ts b/src/router.ts
new file mode 100644
index 0000000..3115160
--- /dev/null
+++ b/src/router.ts
@@ -0,0 +1,1002 @@
+/**
+ * RESTful resource routing middleware for eggjs.
+ */
+
+import { debuglog } from 'node:util';
+import assert from 'node:assert';
+import compose from 'koa-compose';
+import HttpError from 'http-errors';
+import methods from 'methods';
+import { Layer, LayerURLOptions } from './layer.js';
+import { MiddlewareFunc, MiddlewareFuncWithRouter, Next, ParamMiddlewareFunc } from './types.js';
+
+const debug = debuglog('egg-router:router');
+
+export type RouterMethod = typeof methods[0];
+
+export interface RouterOptions {
+  methods?: string[];
+  prefix?: string;
+  sensitive?: boolean;
+  strict?: boolean;
+  routerPath?: string;
+}
+
+export interface RegisterOptions {
+  name?: string;
+  prefix?: string;
+  sensitive?: boolean;
+  strict?: boolean;
+  ignoreCaptures?: boolean;
+  end?: boolean;
+}
+
+export interface AllowedMethodsOptions {
+  throw?: boolean;
+  notImplemented?: () => Error;
+  methodNotAllowed?: () => Error;
+}
+
+export interface MatchedResult {
+  // matched path
+  path: Layer[];
+  // matched path and method(including none method)
+  pathAndMethod: Layer[];
+  // method matched or not
+  route: boolean;
+}
+
+export class Router {
+  readonly opts: RouterOptions;
+  readonly methods: string[];
+  /** Layer stack */
+  readonly stack: Layer[] = [];
+  readonly params: Record<string, ParamMiddlewareFunc> = {};
+
+  /**
+   * Create a new router.
+   *
+   * @example
+   *
+   * Basic usage:
+   *
+   * ```javascript
+   * var Koa = require('koa');
+   * var Router = require('koa-router');
+   *
+   * var app = new Koa();
+   * var router = new Router();
+   *
+   * router.get('/', (ctx, next) => {
+   *   // ctx.router available
+   * });
+   *
+   * app
+   *   .use(router.routes())
+   *   .use(router.allowedMethods());
+   * ```
+   *
+   * @alias module:koa-router
+   * @param {Object=} opts optional
+   * @param {String=} opts.prefix prefix router paths
+   * @class
+   */
+  constructor(opts?: RouterOptions) {
+    this.opts = opts ?? {};
+    this.methods = this.opts.methods ?? [
+      'HEAD',
+      'OPTIONS',
+      'GET',
+      'PUT',
+      'PATCH',
+      'POST',
+      'DELETE',
+    ];
+  }
+
+  /**
+   * 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 path string
+   * @param {Function} middleware middleware function
+   * @return {Router} router instance
+   */
+  use(...middlewares: MiddlewareFunc[]): Router;
+  use(path: string | string[], ...middlewares: MiddlewareFunc[]): Router;
+  use(pathOrMiddleware: string | string[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router {
+    // support array of paths
+    // use(paths, ...middlewares)
+    if (Array.isArray(pathOrMiddleware) && typeof pathOrMiddleware[0] === 'string') {
+      for (const path of pathOrMiddleware) {
+        this.use(path, ...middlewares);
+      }
+      return this;
+    }
+
+    let path = '';
+    let hasPath = false;
+    if (typeof pathOrMiddleware === 'string') {
+      // use(path, ...middlewares)
+      path = pathOrMiddleware;
+      hasPath = true;
+    } else if (typeof pathOrMiddleware === 'function') {
+      // use(...middlewares)
+      middlewares = [ pathOrMiddleware, ...middlewares ];
+    }
+
+    for (const m of middlewares as MiddlewareFuncWithRouter<Router>[]) {
+      if (m.router) {
+        for (const nestedLayer of m.router.stack) {
+          if (path) {
+            nestedLayer.setPrefix(path);
+          }
+          if (this.opts.prefix) {
+            nestedLayer.setPrefix(this.opts.prefix);
+          }
+          this.stack.push(nestedLayer);
+        }
+
+        if (this.params) {
+          for (const key in this.params) {
+            m.router.param(key, this.params[key]);
+          }
+        }
+      } else {
+        this.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
+      }
+    }
+
+    return this;
+  }
+
+  /**
+   * Set the path prefix for a Router instance that was already initialized.
+   *
+   * @example
+   *
+   * ```javascript
+   * router.prefix('/things/:thing_id')
+   * ```
+   *
+   * @param {String} prefix prefix string
+   * @return {Router} router instance
+   */
+  prefix(prefix: string): Router {
+    prefix = prefix.replace(/\/$/, '');
+    this.opts.prefix = prefix;
+
+    for (const layer of this.stack) {
+      layer.setPrefix(prefix);
+    }
+
+    return this;
+  }
+
+  /**
+   * Returns router middleware which dispatches a route matching the request.
+   *
+   * @return {Function} middleware function
+   */
+  routes(): MiddlewareFuncWithRouter<Router> {
+    const dispatch = (ctx: any, next: Next) => {
+      const routerPath: string = this.opts.routerPath || ctx.routerPath || ctx.path;
+      const matched = this.match(routerPath, ctx.method);
+      debug('dispatch: %s %s, routerPath: %s, matched: %s',
+        ctx.method, ctx.path, routerPath, matched.route);
+
+      if (ctx.matched) {
+        (ctx.matched as Layer[]).push(...matched.path);
+      } else {
+        ctx.matched = matched.path;
+      }
+      ctx.router = this;
+
+      if (!matched.route) {
+        return next();
+      }
+
+      const matchedLayers = matched.pathAndMethod;
+      const layerChain = matchedLayers.reduce<MiddlewareFunc[]>((memo, layer) => {
+        memo.push((ctx, next) => {
+          // ctx.captures = layer.captures(routerPath, ctx.captures);
+          ctx.captures = layer.captures(routerPath);
+          ctx.params = layer.params(routerPath, ctx.captures, ctx.params);
+          // ctx._matchedRouteName & ctx._matchedRoute for compatibility
+          ctx._matchedRouteName = ctx.routerName = layer.name;
+          if (!layer.name) {
+            ctx._matchedRouteName = undefined;
+          }
+          ctx._matchedRoute = ctx.routerPath = layer.path;
+          return next();
+        });
+        return memo.concat(layer.stack);
+      }, []);
+
+      return compose(layerChain)(ctx, next);
+    };
+
+    dispatch.router = this;
+    return dispatch;
+  }
+
+  /**
+   * @alias to routes()
+   */
+  middleware() {
+    return this.routes();
+  }
+
+  /**
+   * 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
+   * var Koa = require('koa');
+   * var Router = require('koa-router');
+   *
+   * var app = new Koa();
+   * var router = new Router();
+   *
+   * app.use(router.routes());
+   * app.use(router.allowedMethods());
+   * ```
+   *
+   * **Example with [Boom](https://github.com/hapijs/boom)**
+   *
+   * ```javascript
+   * var Koa = require('koa');
+   * var Router = require('koa-router');
+   * var Boom = require('boom');
+   *
+   * var app = new Koa();
+   * var router = new Router();
+   *
+   * app.use(router.routes());
+   * app.use(router.allowedMethods({
+   *   throw: true,
+   *   notImplemented: () => new Boom.notImplemented(),
+   *   methodNotAllowed: () => new Boom.methodNotAllowed()
+   * }));
+   * ```
+   *
+   * @param {Object=} options optional params
+   * @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
+   * @return {Function} middleware function
+   */
+  allowedMethods(options?: AllowedMethodsOptions): MiddlewareFunc {
+    const implemented = this.methods;
+
+    return async function allowedMethods(ctx: any, next: Next) {
+      await next();
+      if (ctx.status && ctx.status !== 404) return;
+
+      const allowed: Record<string, string> = {};
+      ctx.matched.forEach((route: Router) => {
+        route.methods.forEach(method => {
+          allowed[method] = method;
+        });
+      });
+      const allowedMethods = Object.keys(allowed);
+
+      if (!implemented.includes(ctx.method)) {
+        if (options?.throw) {
+          let notImplementedThrowable: Error;
+          if (typeof options?.notImplemented === 'function') {
+            notImplementedThrowable = options.notImplemented(); // set whatever the user returns from their function
+          } else {
+            notImplementedThrowable = new HttpError.NotImplemented();
+          }
+          throw notImplementedThrowable;
+        } else {
+          ctx.status = 501;
+          ctx.set('Allow', allowedMethods.join(', '));
+        }
+      } else if (allowedMethods.length > 0) {
+        if (ctx.method === 'OPTIONS') {
+          ctx.status = 200;
+          ctx.body = '';
+          ctx.set('Allow', allowedMethods.join(', '));
+        } else if (!allowed[ctx.method]) {
+          if (options?.throw) {
+            let notAllowedThrowable: Error;
+            if (typeof options?.methodNotAllowed === 'function') {
+              notAllowedThrowable = options.methodNotAllowed(); // set whatever the user returns from their function
+            } else {
+              notAllowedThrowable = new HttpError.MethodNotAllowed();
+            }
+            throw notAllowedThrowable;
+          } else {
+            ctx.status = 405;
+            ctx.set('Allow', allowedMethods.join(', '));
+          }
+        }
+      }
+    };
+  }
+
+  /**
+   * 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=} status HTTP status code (default: 301).
+   * @return {Router} router instance
+   */
+  redirect(source: string, destination: string, status: number = 301): Router {
+    // lookup source route by name
+    if (source[0] !== '/') {
+      const routeUrl = this.url(source);
+      if (routeUrl instanceof Error) {
+        throw routeUrl;
+      }
+      source = routeUrl;
+    }
+
+    // lookup destination route by name
+    if (destination[0] !== '/') {
+      const routeUrl = this.url(destination);
+      if (routeUrl instanceof Error) {
+        throw routeUrl;
+      }
+      destination = routeUrl;
+    }
+
+    return this.all(source, ctx => {
+      ctx.redirect(destination);
+      ctx.status = status;
+    });
+  }
+
+  /**
+   * Create and register a route.
+   *
+   * @param {String|String[]} path Path string.
+   * @param {String[]} methods Array of HTTP verbs.
+   * @param {Function|Function[]} middleware Multiple middleware also accepted.
+   * @param {Object} [opts] optional params
+   * @private
+   */
+  register(path: string | string[] | RegExp | RegExp[],
+    methods: string[],
+    middleware: MiddlewareFunc | MiddlewareFunc[],
+    opts?: RegisterOptions): Layer | Layer[] {
+    opts = opts ?? {};
+    // support array of paths
+    if (Array.isArray(path)) {
+      const routes: Layer[] = [];
+      for (const p of path) {
+        const route = this.register(p, methods, middleware, opts) as Layer;
+        routes.push(route);
+      }
+      return routes;
+    }
+
+    // create route
+    const route = new Layer(path, methods, middleware, {
+      end: opts.end === false ? opts.end : true,
+      name: opts.name,
+      sensitive: opts.sensitive ?? this.opts.sensitive ?? false,
+      strict: opts.strict ?? this.opts.strict ?? false,
+      prefix: opts.prefix ?? this.opts.prefix ?? '',
+      ignoreCaptures: opts.ignoreCaptures,
+    });
+
+    // FIXME: why???
+    if (this.opts.prefix) {
+      route.setPrefix(this.opts.prefix);
+    }
+
+    // add parameter middleware to the new route layer
+    for (const param in this.params) {
+      route.param(param, this.params[param]);
+    }
+
+    this.stack.push(route);
+    return route;
+  }
+
+  /**
+   * Lookup route with given `name`.
+   *
+   * @param {String} name route name
+   * @return {Layer|false} layer instance of false
+   */
+  route(name: string): Layer | false {
+    for (const route of this.stack) {
+      if (route.name === name) {
+        return route;
+      }
+    }
+    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
+   * @return {String|Error} string or error instance
+   */
+  url(name: string, params?: string | number | object,
+    ...paramsOrOptions: (string | number | object | LayerURLOptions)[]): string | Error {
+    const route = this.route(name);
+    if (route) {
+      return route.url(params, ...paramsOrOptions);
+    }
+    return new Error(`No route found for name: ${name}`);
+  }
+
+  /**
+   * Generate URL from url pattern and given `params`.
+   *
+   * @example
+   *
+   * ```javascript
+   * var url = Router.url('/users/:id', { id: 1 });
+   * // => "/users/1"
+   * ```
+   *
+   * @param {String} path url pattern
+   * @param {Object} params url parameters
+   * @return {String} url string
+   */
+  static url(path: string, params?: string | number | object,
+    ...paramsOrOptions: (string | number | object | LayerURLOptions)[]): string {
+    return Layer.prototype.url.call({ path }, params, ...paramsOrOptions);
+  }
+
+  /**
+   * Match given `path` and return corresponding routes.
+   *
+   * @param {String} path path string
+   * @param {String} method method name
+   * @return {Object.<path, pathAndMethod>} returns layers that matched path and
+   * path and method.
+   * @private
+   */
+  match(path: string, method: string): MatchedResult {
+    const matched: MatchedResult = {
+      // matched path
+      path: [],
+      // matched path and method(including none method)
+      pathAndMethod: [],
+      // method matched or not
+      route: false,
+    };
+
+    for (const layer of this.stack) {
+      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;
+          }
+        }
+        // if (layer.methods.length === 0) {
+        //   matched.pathAndMethod.push(layer);
+        // } else if (layer.methods.includes(method)) {
+        //   matched.pathAndMethod.push(layer);
+        //   matched.route = true;
+        // }
+      }
+    }
+
+    return matched;
+  }
+
+  /**
+   * 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
+   * @param {Function} middleware route middleware
+   * @return {Router} instance
+   */
+  param(param: string, middleware: ParamMiddlewareFunc): Router {
+    this.params[param] = middleware;
+    for (const route of this.stack) {
+      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
+   * var forums = new Router();
+   * var 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
+   * var 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.
+   *
+   * @name get|put|post|patch|delete|del
+   * @memberof module:koa-router.prototype
+   * @param {String} method http method
+   * @param {String} nameOrPath http path
+   * @param {Function=} pathOrMiddleware route middleware(s)
+   * @param {Function} middlewares middlewares
+   * @return {Router} Router instance
+   */
+  verb(method: RouterMethod | RouterMethod[], nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    const options: RegisterOptions = {};
+    let path: string | RegExp;
+    if (typeof pathOrMiddleware === 'string' || pathOrMiddleware instanceof RegExp) {
+      // verb(method, name, path, ...middlewares)
+      path = pathOrMiddleware;
+      assert(typeof nameOrPath === 'string', 'route name should be string');
+      options.name = nameOrPath;
+    } else {
+      // verb(method, path, ...middlewares)
+      path = nameOrPath;
+      middlewares = [ pathOrMiddleware, ...middlewares ];
+    }
+    if (typeof method === 'string') {
+      method = [ method ];
+    }
+    this.register(path, method, middlewares, options);
+    return this;
+  }
+
+  /**
+   * Register route with all methods.
+   *
+   * @param {String} name Optional.
+   * @param {String} path path string
+   * @param {Function=} middleware You may also pass multiple middleware.
+   * @return {Router} router instance
+   * @private
+   */
+  all(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  all(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  all(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb(methods, nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  // "acl", "bind", "checkout", "connect", "copy", "delete", "get", "head", "link", "lock",
+  // "m-search", "merge", "mkactivity", "mkcalendar", "mkcol", "move", "notify", "options",
+  // "patch", "post", "propfind", "proppatch", "purge", "put", "rebind", "report", "search",
+  // "source", "subscribe", "trace", "unbind", "unlink", "unlock", "unsubscribe"
+  acl(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  acl(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  acl(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('acl', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  bind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  bind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  bind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('bind', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  checkout(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  checkout(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  checkout(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('checkout', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  connect(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  connect(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  connect(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('connect', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  copy(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  copy(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  copy(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('copy', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  delete(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  delete(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  delete(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('delete', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  /** Alias for `router.delete()` because delete is a reserved word */
+  del(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  del(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  del(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('delete', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  get(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  get(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  get(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('get', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  head(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  head(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  head(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('head', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  link(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  link(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  link(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('link', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  lock(path: string, ...middlewares: MiddlewareFunc[]): Router;
+  lock(name: string, path: string, ...middlewares: MiddlewareFunc[]): Router;
+  lock(nameOrPath: string, pathOrMiddleware: string | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('lock', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  ['m-search'](path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  ['m-search'](name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  ['m-search'](nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('m-search', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  merge(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  merge(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  merge(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('merge', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  mkactivity(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  mkactivity(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  mkactivity(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('mkactivity', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  mkcalendar(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  mkcalendar(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  mkcalendar(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('mkcalendar', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  mkcol(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  mkcol(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  mkcol(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('mkcol', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  move(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  move(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  move(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('move', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  notify(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  notify(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  notify(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('notify', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  options(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  options(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  options(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('options', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  patch(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  patch(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  patch(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('patch', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  post(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  post(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  post(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('post', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  propfind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  propfind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  propfind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('propfind', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  proppatch(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  proppatch(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  proppatch(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('proppatch', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  purge(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  purge(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  purge(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('purge', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  put(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  put(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  put(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('put', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  rebind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  rebind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  rebind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('rebind', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  report(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  report(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  report(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('report', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  search(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  search(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  search(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('search', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  source(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  source(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  source(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('source', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  subscribe(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  subscribe(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  subscribe(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('subscribe', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  trace(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  trace(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  trace(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('trace', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  unbind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  unbind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  unbind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('unbind', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  unlink(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  unlink(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  unlink(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('unlink', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  unlock(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  unlock(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  unlock(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('unlock', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  unsubscribe(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  unsubscribe(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
+  unsubscribe(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('unsubscribe', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+}
diff --git a/src/types.ts b/src/types.ts
index 5cb66f7..b0311c0 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -2,3 +2,4 @@ export type Next = () => Promise<void>;
 export type MiddlewareFunc = (ctx: any, next: Next) => Promise<void> | void;
 export type MiddlewareFuncWithParamProperty = MiddlewareFunc & { param?: string };
 export type ParamMiddlewareFunc = (param: string, ctx: any, next: Next) => Promise<void> | void;
+export type MiddlewareFuncWithRouter<T> = MiddlewareFunc & { router: T };
diff --git a/src/utils.ts b/src/utils.ts
index b8bd1f2..8348173 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,6 +1,9 @@
 import { isFunction, isGeneratorFunction } from 'is-type-of';
+import { MiddlewareFunc } from './types.js';
 
-export async function callFn(fn: Function, args: any[], ctx: unknown): Promise<unknown> {
+type Fn = (...args: any[]) => any;
+
+export async function callFn(fn: Fn, args: any[], ctx: unknown) {
   args = args || [];
   if (!isFunction(fn)) {
     return;
@@ -11,7 +14,7 @@ export async function callFn(fn: Function, args: any[], ctx: unknown): Promise<u
   return ctx ? fn.call(ctx, ...args) : fn(...args);
 }
 
-export function middleware(fn: Function) {
+export function middleware(fn: MiddlewareFunc) {
   if (isGeneratorFunction(fn)) {
     throw new TypeError(`Please use async function instead of generator function: ${fn.toString()}`);
   }
diff --git a/test/lib/layer.test.ts b/test/lib/layer.test.ts
index c11ebe1..e11eb3a 100644
--- a/test/lib/layer.test.ts
+++ b/test/lib/layer.test.ts
@@ -1,171 +1,162 @@
+import { strict as assert } from 'node:assert';
 import Application from '@eggjs/koa';
-import http from 'http';
 import request from 'supertest';
-import Router from '../../lib/router';
-import Layer from '../../lib/layer';
+import { Router } from '../../src/router.js';
+import { Layer } from '../../src/layer.js';
 
 describe('test/lib/layer.test.js', () => {
-  it('composes multiple callbacks/middlware', function(done) {
+  it('composes multiple callbacks/middleware', async () => {
     const app = new Application();
     const router = new Router();
     app.use(router.routes());
     router.get(
       '/:category/:title',
-      function(ctx, next) {
+      (ctx, next) => {
         ctx.status = 500;
         return next();
       },
-      function(ctx, next) {
+      (ctx, next) => {
         ctx.status = 204;
         return next();
       },
     );
-    request(http.createServer(app.callback()))
+    await request(app.callback())
       .get('/programming/how-to-node')
-      .expect(204)
-      .end(function(err) {
-        if (err) return done(err);
-        done();
-      });
+      .expect(204);
   });
 
-  describe('Layer#match()', function() {
-    it('captures URL path parameters', function(done) {
-      const app = new Koa();
+  describe('Layer#match()', () => {
+    it('captures URL path parameters', async () => {
+      const app = new Application();
       const router = new Router();
       app.use(router.routes());
-      router.get('/:category/:title', function(ctx) {
-        ctx.should.have.property('params');
-        ctx.params.should.be.type('object');
-        ctx.params.should.have.property('category', 'match');
-        ctx.params.should.have.property('title', 'this');
+      router.get('/:category/:title', ctx => {
+        assert(ctx.params);
+        assert.equal(ctx.params.category, 'match');
+        assert.equal(ctx.params.title, 'this');
         ctx.status = 204;
       });
-      request(http.createServer(app.callback()))
+      await request(app.callback())
         .get('/match/this')
-        .expect(204)
-        .end(function(err) {
-          if (err) return done(err);
-          done();
-        });
+        .expect(204);
     });
 
-    it('return orginal path parameters when decodeURIComponent throw error', function(done) {
-      const app = new Koa();
+    it('return original path parameters when decodeURIComponent throw error', async () => {
+      const app = new Application();
       const router = new Router();
       app.use(router.routes());
-      router.get('/:category/:title', function(ctx) {
-        ctx.should.have.property('params');
-        ctx.params.should.be.type('object');
-        ctx.params.should.have.property('category', '100%');
-        ctx.params.should.have.property('title', '101%');
+      router.get('/:category/:title', ctx => {
+        assert(ctx.params);
+        assert.equal(ctx.params.category, '100%');
+        assert.equal(ctx.params.title, '101%');
         ctx.status = 204;
       });
-      request(http.createServer(app.callback()))
+      await request(app.callback())
         .get('/100%/101%')
-        .expect(204)
-        .end(done);
+        .expect(204);
     });
 
-    it('populates ctx.captures with regexp captures', function(done) {
-      const app = new Koa();
+    it('populates ctx.captures with regexp captures', async () => {
+      const app = new Application();
       const router = new Router();
       app.use(router.routes());
-      router.get(/^\/api\/([^\/]+)\/?/i, function(ctx, next) {
-        ctx.should.have.property('captures');
-        ctx.captures.should.be.instanceOf(Array);
-        ctx.captures.should.have.property(0, '1');
+      router.get(/^\/api\/([^\/]+)\/?/i, (ctx, next) => {
+        assert(ctx.captures);
+        assert(Array.isArray(ctx.captures));
+        assert.equal(ctx.captures.length, 1);
+        assert.equal(ctx.captures[0], '1');
         return next();
-      }, function(ctx) {
-        ctx.should.have.property('captures');
-        ctx.captures.should.be.instanceOf(Array);
-        ctx.captures.should.have.property(0, '1');
+      }, ctx => {
+        assert(ctx.captures);
+        assert(Array.isArray(ctx.captures));
+        assert.equal(ctx.captures.length, 1);
+        assert.equal(ctx.captures[0], '1');
         ctx.status = 204;
       });
-      request(http.createServer(app.callback()))
+      await request(app.callback())
         .get('/api/1')
-        .expect(204)
-        .end(function(err) {
-          if (err) return done(err);
-          done();
-        });
+        .expect(204);
     });
 
-    it('return orginal ctx.captures when decodeURIComponent throw error', function(done) {
-      const app = new Koa();
+    it('return original ctx.captures when decodeURIComponent throw error', async () => {
+      const app = new Application();
       const router = new Router();
       app.use(router.routes());
-      router.get(/^\/api\/([^\/]+)\/?/i, function(ctx, next) {
-        ctx.should.have.property('captures');
-        ctx.captures.should.be.type('object');
-        ctx.captures.should.have.property(0, '101%');
+      router.get(/^\/api\/([^\/]+)\/?/i, (ctx, next) => {
+        assert(Array.isArray(ctx.captures));
+        assert.equal(ctx.captures.length, 1);
+        assert.equal(ctx.captures[0], '101%');
         return next();
       }, function(ctx) {
-        ctx.should.have.property('captures');
-        ctx.captures.should.be.type('object');
-        ctx.captures.should.have.property(0, '101%');
+        assert(Array.isArray(ctx.captures));
+        assert.equal(ctx.captures.length, 1);
+        assert.equal(ctx.captures[0], '101%');
         ctx.status = 204;
       });
-      request(http.createServer(app.callback()))
+      await request(app.callback())
         .get('/api/101%')
-        .expect(204)
-        .end(function(err) {
-          if (err) return done(err);
-          done();
-        });
+        .expect(204);
     });
 
-    it('populates ctx.captures with regexp captures include undefiend', function(done) {
-      const app = new Koa();
+    it('populates ctx.captures with regexp captures include undefined', async () => {
+      const app = new Application();
       const router = new Router();
       app.use(router.routes());
       router.get(/^\/api(\/.+)?/i, function(ctx, next) {
-        ctx.should.have.property('captures');
-        ctx.captures.should.be.type('object');
-        ctx.captures.should.have.property(0, undefined);
+        assert(Array.isArray(ctx.captures));
+        assert.equal(ctx.captures.length, 1);
+        assert.equal(ctx.captures[0], undefined);
         return next();
       }, function(ctx) {
-        ctx.should.have.property('captures');
-        ctx.captures.should.be.type('object');
-        ctx.captures.should.have.property(0, undefined);
+        assert(Array.isArray(ctx.captures));
+        assert.equal(ctx.captures.length, 1);
+        assert.equal(ctx.captures[0], undefined);
         ctx.status = 204;
       });
-      request(http.createServer(app.callback()))
+      await request(app.callback())
         .get('/api')
-        .expect(204)
-        .end(function(err) {
-          if (err) return done(err);
-          done();
-        });
+        .expect(204);
     });
 
-    it('should throw friendly error message when handle not exists', function() {
-      const app = new Koa();
+    it('should throw friendly error message when handle not exists', () => {
+      const app = new Application();
       const router = new Router();
       app.use(router.routes());
-      let notexistHandle;
-      (function() {
-        router.get('/foo', notexistHandle);
-      }).should.throw('get `/foo`: `middleware` must be a function, not `undefined`');
+      const notExistsHandle = undefined;
 
-      (function() {
-        router.get('foo router', '/foo', notexistHandle);
-      }).should.throw('get `foo router`: `middleware` must be a function, not `undefined`');
+      assert.throws(() => {
+        router.get('/foo', notExistsHandle as any);
+      }, (err: TypeError) => {
+        assert(err instanceof TypeError);
+        assert.equal(err.name, 'TypeError');
+        assert.equal(err.message, 'get `/foo`: `middleware` must be a function, not `undefined`');
+        return true;
+      });
+
+      assert.throws(() => {
+        router.get('foo router', '/foo', notExistsHandle as any);
+      }, (err: any) => {
+        assert.equal(err.message, 'get `foo router`: `middleware` must be a function, not `undefined`');
+        return true;
+      });
 
-      (function() {
-        router.post('/foo', function() {}, notexistHandle);
-      }).should.throw('post `/foo`: `middleware` must be a function, not `undefined`');
+      assert.throws(() => {
+        router.post('/foo', function() {}, notExistsHandle as any);
+      }, (err: any) => {
+        assert.equal(err.message, 'post `/foo`: `middleware` must be a function, not `undefined`');
+        return true;
+      });
     });
   });
 
-  describe('Layer#param()', function() {
-    it('composes middleware for param fn', function(done) {
-      const app = new Koa();
+  describe('Layer#param()', () => {
+    it('composes middleware for param fn', async () => {
+      const app = new Application();
       const router = new Router();
       const route = new Layer('/users/:user', [ 'GET' ], [ function(ctx) {
         ctx.body = ctx.user;
       } ]);
-      route.param('user', function(id, ctx, next) {
+      route.param('user', (id, ctx, next) => {
         ctx.user = { name: 'alex' };
         if (!id) {
           ctx.status = 404;
@@ -175,21 +166,16 @@ describe('test/lib/layer.test.js', () => {
       });
       router.stack.push(route);
       app.use(router.middleware());
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .get('/users/3')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          res.should.have.property('body');
-          res.body.should.have.property('name', 'alex');
-          done();
-        });
+        .expect(200);
+      assert.equal(res.body.name, 'alex');
     });
 
-    it('ignores params which are not matched', function(done) {
-      const app = new Koa();
+    it('ignores params which are not matched', async () => {
+      const app = new Application();
       const router = new Router();
-      const route = new Layer('/users/:user', [ 'GET' ], [ function(ctx) {
+      const route = new Layer('/users/:user', [ 'GET' ], [ ctx => {
         ctx.body = ctx.user;
       } ]);
       route.param('user', function(id, ctx, next) {
@@ -210,31 +196,26 @@ describe('test/lib/layer.test.js', () => {
       });
       router.stack.push(route);
       app.use(router.middleware());
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .get('/users/3')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          res.should.have.property('body');
-          res.body.should.have.property('name', 'alex');
-          done();
-        });
+        .expect(200);
+      assert.equal(res.body.name, 'alex');
     });
   });
 
-  describe('Layer#url()', function() {
-    it('generates route URL', function() {
+  describe('Layer#url()', () => {
+    it('generates route URL', () => {
       const route = new Layer('/:category/:title', [ 'get' ], [ function() {} ], 'books');
-      let url = route.url({ category: 'programming', title: 'how-to-node' });
-      url.should.equal('/programming/how-to-node');
-      url = route.url('programming', 'how-to-node');
-      url.should.equal('/programming/how-to-node');
+      const url1 = route.url({ category: 'programming', title: 'how-to-node' });
+      assert.equal(url1, '/programming/how-to-node');
+      const url2 = route.url('programming', 'how-to-node');
+      assert.equal(url2, '/programming/how-to-node');
     });
 
-    it('escapes using encodeURIComponent()', function() {
-      const route = new Layer('/:category/:title', [ 'get' ], [ function() {} ], 'books');
+    it('escapes using encodeURIComponent()', () => {
+      const route = new Layer('/:category/:title', [ 'get' ], [ () => {} ], 'books');
       const url = route.url({ category: 'programming', title: 'how to node' });
-      url.should.equal('/programming/how%20to%20node');
+      assert.equal(url, '/programming/how%20to%20node');
     });
   });
 });

From 8d7113bed1a6993c2762305ac416f2e9eb9a5ab8 Mon Sep 17 00:00:00 2001
From: fengmk2 <suqian.yf@antgroup.com>
Date: Mon, 10 Jun 2024 21:26:09 +0800
Subject: [PATCH 04/13] f

---
 bench/{server.js => server.cjs}      |  7 +++----
 package.json                         |  8 ++++----
 src/{layer.ts => Layer2.ts}          |  0
 src/{egg_router.js => egg_router.ts} | 14 ++++++--------
 src/index.ts                         |  4 ++++
 src/router.ts                        |  2 +-
 test/{lib => }/egg_router.test.js    |  2 +-
 test/index.test.js                   | 12 ------------
 test/index.test.ts                   | 10 ++++++++++
 test/{lib => }/layer.test.ts         |  6 +++---
 test/{lib => }/router.test.js        |  0
 test/{lib => }/utils.test.js         |  0
 12 files changed, 32 insertions(+), 33 deletions(-)
 rename bench/{server.js => server.cjs} (90%)
 rename src/{layer.ts => Layer2.ts} (100%)
 rename src/{egg_router.js => egg_router.ts} (97%)
 rename test/{lib => }/egg_router.test.js (99%)
 delete mode 100644 test/index.test.js
 create mode 100644 test/index.test.ts
 rename test/{lib => }/layer.test.ts (98%)
 rename test/{lib => }/router.test.js (100%)
 rename test/{lib => }/utils.test.js (100%)

diff --git a/bench/server.js b/bench/server.cjs
similarity index 90%
rename from bench/server.js
rename to bench/server.cjs
index 9464242..4d4dbdf 100644
--- a/bench/server.js
+++ b/bench/server.cjs
@@ -1,7 +1,6 @@
-'use strict';
-
-const Koa = require('koa');
-const Router = require('../');
+/* eslint-disable @typescript-eslint/no-var-requires */
+const Koa = require('@eggjs/koa');
+const Router = require('../dist/commonjs');
 
 const app = new Koa();
 const router = new Router();
diff --git a/package.json b/package.json
index 1027993..149e16b 100644
--- a/package.json
+++ b/package.json
@@ -60,6 +60,10 @@
     "prepublishOnly": "tshy && tshy-after"
   },
   "license": "MIT",
+  "files": [
+    "dist",
+    "src"
+  ],
   "type": "module",
   "tshy": {
     "exports": {
@@ -82,10 +86,6 @@
       }
     }
   },
-  "files": [
-    "dist",
-    "src"
-  ],
   "main": "./dist/commonjs/index.js",
   "types": "./dist/commonjs/index.d.ts"
 }
diff --git a/src/layer.ts b/src/Layer2.ts
similarity index 100%
rename from src/layer.ts
rename to src/Layer2.ts
diff --git a/src/egg_router.js b/src/egg_router.ts
similarity index 97%
rename from src/egg_router.js
rename to src/egg_router.ts
index fef7c9f..75b83bb 100644
--- a/src/egg_router.js
+++ b/src/egg_router.ts
@@ -1,11 +1,9 @@
-'use strict';
-
-const is = require('is-type-of');
-const Router = require('./router');
-const utility = require('utility');
-const inflection = require('inflection');
-const assert = require('assert');
-const utils = require('./utils');
+import is from 'is-type-of';
+import Router from './router';
+import utility from 'utility';
+import inflection from 'inflection';
+import assert from 'assert';
+import utils from './utils';
 
 const METHODS = [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete' ];
 
diff --git a/src/index.ts b/src/index.ts
index 54a0bbe..e12fbb6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,7 @@
+import { Router } from './router.js';
+
 export * from './router.js';
 // import EggRouter from './lib/egg_router';
 
+export const KoaRouter = Router;
+export default Router;
diff --git a/src/router.ts b/src/router.ts
index 3115160..3659d5f 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -7,7 +7,7 @@ import assert from 'node:assert';
 import compose from 'koa-compose';
 import HttpError from 'http-errors';
 import methods from 'methods';
-import { Layer, LayerURLOptions } from './layer.js';
+import { Layer, LayerURLOptions } from './Layer2.js';
 import { MiddlewareFunc, MiddlewareFuncWithRouter, Next, ParamMiddlewareFunc } from './types.js';
 
 const debug = debuglog('egg-router:router');
diff --git a/test/lib/egg_router.test.js b/test/egg_router.test.js
similarity index 99%
rename from test/lib/egg_router.test.js
rename to test/egg_router.test.js
index 381691a..8a729de 100644
--- a/test/lib/egg_router.test.js
+++ b/test/egg_router.test.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const EggRouter = require('../../').EggRouter;
+const EggRouter = require('../dist/commonjs').EggRouter;
 const assert = require('assert');
 const is = require('is-type-of');
 
diff --git a/test/index.test.js b/test/index.test.js
deleted file mode 100644
index 17cb5ee..0000000
--- a/test/index.test.js
+++ /dev/null
@@ -1,12 +0,0 @@
-'use strict';
-
-const assert = require('assert');
-const Router = require('..');
-
-describe('test/index.test.js', () => {
-  it('should expose Router', () => {
-    assert(typeof Router === 'function');
-    assert(typeof Router.KoaRouter === 'function');
-    assert(typeof Router.EggRouter === 'function');
-  });
-});
diff --git a/test/index.test.ts b/test/index.test.ts
new file mode 100644
index 0000000..837e500
--- /dev/null
+++ b/test/index.test.ts
@@ -0,0 +1,10 @@
+import { strict as assert } from 'node:assert';
+import Router, { KoaRouter } from '../src/index.js';
+
+describe('test/index.test.ts', () => {
+  it('should expose Router', () => {
+    assert(typeof Router === 'function');
+    assert(typeof KoaRouter === 'function');
+    // assert(typeof Router.EggRouter === 'function');
+  });
+});
diff --git a/test/lib/layer.test.ts b/test/layer.test.ts
similarity index 98%
rename from test/lib/layer.test.ts
rename to test/layer.test.ts
index e11eb3a..926318f 100644
--- a/test/lib/layer.test.ts
+++ b/test/layer.test.ts
@@ -1,10 +1,10 @@
 import { strict as assert } from 'node:assert';
 import Application from '@eggjs/koa';
 import request from 'supertest';
-import { Router } from '../../src/router.js';
-import { Layer } from '../../src/layer.js';
+import { Router } from '../src/router.js';
+import { Layer } from '../src/Layer2.js';
 
-describe('test/lib/layer.test.js', () => {
+describe('test/layer.test.ts', () => {
   it('composes multiple callbacks/middleware', async () => {
     const app = new Application();
     const router = new Router();
diff --git a/test/lib/router.test.js b/test/router.test.js
similarity index 100%
rename from test/lib/router.test.js
rename to test/router.test.js
diff --git a/test/lib/utils.test.js b/test/utils.test.js
similarity index 100%
rename from test/lib/utils.test.js
rename to test/utils.test.js

From 924e4e187c56c5bd3a95187fefadc60817f643a0 Mon Sep 17 00:00:00 2001
From: fengmk2 <suqian.yf@antgroup.com>
Date: Mon, 10 Jun 2024 21:26:59 +0800
Subject: [PATCH 05/13] f

---
 src/{Layer2.ts => Layer.ts}            | 0
 src/router.ts                          | 2 +-
 test/{layer.test.ts => Layer2.test.ts} | 4 ++--
 3 files changed, 3 insertions(+), 3 deletions(-)
 rename src/{Layer2.ts => Layer.ts} (100%)
 rename test/{layer.test.ts => Layer2.test.ts} (98%)

diff --git a/src/Layer2.ts b/src/Layer.ts
similarity index 100%
rename from src/Layer2.ts
rename to src/Layer.ts
diff --git a/src/router.ts b/src/router.ts
index 3659d5f..643d13e 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -7,7 +7,7 @@ import assert from 'node:assert';
 import compose from 'koa-compose';
 import HttpError from 'http-errors';
 import methods from 'methods';
-import { Layer, LayerURLOptions } from './Layer2.js';
+import { Layer, LayerURLOptions } from './Layer.js';
 import { MiddlewareFunc, MiddlewareFuncWithRouter, Next, ParamMiddlewareFunc } from './types.js';
 
 const debug = debuglog('egg-router:router');
diff --git a/test/layer.test.ts b/test/Layer2.test.ts
similarity index 98%
rename from test/layer.test.ts
rename to test/Layer2.test.ts
index 926318f..fd12f45 100644
--- a/test/layer.test.ts
+++ b/test/Layer2.test.ts
@@ -2,9 +2,9 @@ import { strict as assert } from 'node:assert';
 import Application from '@eggjs/koa';
 import request from 'supertest';
 import { Router } from '../src/router.js';
-import { Layer } from '../src/Layer2.js';
+import { Layer } from '../src/Layer.js';
 
-describe('test/layer.test.ts', () => {
+describe('test/Layer.test.ts', () => {
   it('composes multiple callbacks/middleware', async () => {
     const app = new Application();
     const router = new Router();

From 9e8e9e9b044ddeb4b395e63fcffd95f50839fada Mon Sep 17 00:00:00 2001
From: fengmk2 <suqian.yf@antgroup.com>
Date: Mon, 10 Jun 2024 21:27:12 +0800
Subject: [PATCH 06/13] f

---
 test/{Layer2.test.ts => Layer.test.ts} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename test/{Layer2.test.ts => Layer.test.ts} (100%)

diff --git a/test/Layer2.test.ts b/test/Layer.test.ts
similarity index 100%
rename from test/Layer2.test.ts
rename to test/Layer.test.ts

From 1462978d0a331ff70f2def74985f54c23e13e4df Mon Sep 17 00:00:00 2001
From: fengmk2 <suqian.yf@antgroup.com>
Date: Mon, 10 Jun 2024 21:28:31 +0800
Subject: [PATCH 07/13] f

---
 src/{egg_router.ts => EggRouter.ts} | 0
 src/{router.ts => Router2.ts}       | 0
 src/index.ts                        | 4 ++--
 test/Layer.test.ts                  | 2 +-
 4 files changed, 3 insertions(+), 3 deletions(-)
 rename src/{egg_router.ts => EggRouter.ts} (100%)
 rename src/{router.ts => Router2.ts} (100%)

diff --git a/src/egg_router.ts b/src/EggRouter.ts
similarity index 100%
rename from src/egg_router.ts
rename to src/EggRouter.ts
diff --git a/src/router.ts b/src/Router2.ts
similarity index 100%
rename from src/router.ts
rename to src/Router2.ts
diff --git a/src/index.ts b/src/index.ts
index e12fbb6..2b057df 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,6 @@
-import { Router } from './router.js';
+import { Router } from './Router2.js';
 
-export * from './router.js';
+export * from './Router2.js';
 // import EggRouter from './lib/egg_router';
 
 export const KoaRouter = Router;
diff --git a/test/Layer.test.ts b/test/Layer.test.ts
index fd12f45..c4931b5 100644
--- a/test/Layer.test.ts
+++ b/test/Layer.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import Application from '@eggjs/koa';
 import request from 'supertest';
-import { Router } from '../src/router.js';
+import { Router } from '../src/Router2.js';
 import { Layer } from '../src/Layer.js';
 
 describe('test/Layer.test.ts', () => {

From 44219c48e5da0cc55024a7a113a0b775af50e2ae Mon Sep 17 00:00:00 2001
From: fengmk2 <suqian.yf@antgroup.com>
Date: Mon, 10 Jun 2024 21:29:20 +0800
Subject: [PATCH 08/13] f

---
 src/{Router2.ts => Router.ts}                  | 0
 src/index.ts                                   | 4 ++--
 test/{egg_router.test.js => EggRouter.test.ts} | 0
 test/Layer.test.ts                             | 2 +-
 test/{router.test.js => Router.test.ts}        | 0
 test/{utils.test.js => utils.test.ts}          | 0
 6 files changed, 3 insertions(+), 3 deletions(-)
 rename src/{Router2.ts => Router.ts} (100%)
 rename test/{egg_router.test.js => EggRouter.test.ts} (100%)
 rename test/{router.test.js => Router.test.ts} (100%)
 rename test/{utils.test.js => utils.test.ts} (100%)

diff --git a/src/Router2.ts b/src/Router.ts
similarity index 100%
rename from src/Router2.ts
rename to src/Router.ts
diff --git a/src/index.ts b/src/index.ts
index 2b057df..5e3332e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,6 @@
-import { Router } from './Router2.js';
+import { Router } from './Router.js';
 
-export * from './Router2.js';
+export * from './Router.js';
 // import EggRouter from './lib/egg_router';
 
 export const KoaRouter = Router;
diff --git a/test/egg_router.test.js b/test/EggRouter.test.ts
similarity index 100%
rename from test/egg_router.test.js
rename to test/EggRouter.test.ts
diff --git a/test/Layer.test.ts b/test/Layer.test.ts
index c4931b5..df20073 100644
--- a/test/Layer.test.ts
+++ b/test/Layer.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import Application from '@eggjs/koa';
 import request from 'supertest';
-import { Router } from '../src/Router2.js';
+import { Router } from '../src/Router.js';
 import { Layer } from '../src/Layer.js';
 
 describe('test/Layer.test.ts', () => {
diff --git a/test/router.test.js b/test/Router.test.ts
similarity index 100%
rename from test/router.test.js
rename to test/Router.test.ts
diff --git a/test/utils.test.js b/test/utils.test.ts
similarity index 100%
rename from test/utils.test.js
rename to test/utils.test.ts

From 54f16c733ff891222696b1239cbc36976461ab4c Mon Sep 17 00:00:00 2001
From: fengmk2 <suqian.yf@antgroup.com>
Date: Tue, 11 Jun 2024 14:21:08 +0800
Subject: [PATCH 09/13] f

---
 src/EggRouter.ts       |  266 ++++-----
 src/Layer.ts           |   25 +-
 src/Router.ts          |  308 ++++++-----
 src/index.ts           |    2 +-
 src/types.ts           |   10 +
 src/utils.ts           |   22 -
 test/EggRouter.test.ts |   83 +--
 test/Router.test.ts    | 1160 +++++++++++++++-------------------------
 test/index.test.ts     |    5 +-
 test/utils.test.ts     |   61 ---
 10 files changed, 835 insertions(+), 1107 deletions(-)
 delete mode 100644 src/utils.ts
 delete mode 100644 test/utils.test.ts

diff --git a/src/EggRouter.ts b/src/EggRouter.ts
index 75b83bb..f5e38b6 100644
--- a/src/EggRouter.ts
+++ b/src/EggRouter.ts
@@ -1,13 +1,18 @@
-import is from 'is-type-of';
-import Router from './router';
-import utility from 'utility';
+import assert from 'node:assert';
+import { encodeURIComponent as safeEncodeURIComponent } from 'utility';
 import inflection from 'inflection';
-import assert from 'assert';
-import utils from './utils';
+import methods from 'methods';
+import { RegisterOptions, Router, RouterMethod, RouterOptions } from './Router.js';
+import { MiddlewareFunc, ResourcesController } from './types.js';
 
-const METHODS = [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete' ];
+interface RestfulOptions {
+  suffix?: string;
+  namePrefix?: string;
+  method: string | string[];
+  member?: true;
+}
 
-const REST_MAP = {
+const REST_MAP: Record<string, RestfulOptions> = {
   index: {
     suffix: '',
     method: 'GET',
@@ -47,51 +52,117 @@ const REST_MAP = {
   },
 };
 
+interface Application {
+  controller: Record<string, any>;
+}
+
 /**
  * FIXME: move these patch into @eggjs/router
  */
-class EggRouter extends Router {
+export class EggRouter extends Router {
+  readonly app: Application;
 
   /**
    * @class
    * @param {Object} opts - Router options.
    * @param {Application} app - Application object.
    */
-  constructor(opts, app) {
+  constructor(opts: RouterOptions, app: Application) {
     super(opts);
     this.app = app;
-    this.patchRouterMethod();
   }
 
-  patchRouterMethod() {
-    // patch router methods to support generator function middleware and string controller
-    METHODS.concat([ 'all' ]).forEach(method => {
-      this[method] = (...args) => {
-        const splited = spliteAndResolveRouterParams({ args, app: this.app });
-        // format and rebuild params
-        args = splited.prefix.concat(splited.middlewares);
-        return super[method](...args);
-      };
-    });
+  #formatRouteParams(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc | ResourcesController,
+    middlewares: (MiddlewareFunc | string | ResourcesController)[]) {
+    const options: RegisterOptions = {};
+    let path: string | RegExp;
+    if (typeof pathOrMiddleware === 'string' || pathOrMiddleware instanceof RegExp) {
+      // verb(method, name, path, ...middlewares)
+      path = pathOrMiddleware;
+      assert(typeof nameOrPath === 'string', 'route name should be string');
+      options.name = nameOrPath;
+    } else {
+      // verb(method, path, ...middlewares)
+      path = nameOrPath;
+      middlewares = [ pathOrMiddleware, ...middlewares ];
+    }
+    return {
+      path,
+      middlewares,
+      options,
+    };
   }
 
-  /**
-   * Create and register a route.
-   * @param {String} path - url path
-   * @param {Array} methods - Array of HTTP verbs
-   * @param {Array} middlewares -
-   * @param {Object} opts -
-   * @return {Route} this
-   */
-  register(path, methods, middlewares, opts) {
-    // patch register to support generator function middleware and string controller
-    middlewares = Array.isArray(middlewares) ? middlewares : [ middlewares ];
-    middlewares = convertMiddlewares(middlewares, this.app);
-    path = Array.isArray(path) ? path : [ path ];
-    path.forEach(p => super.register(p, methods, middlewares, opts));
+  verb(method: RouterMethod | RouterMethod[], nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middleware: (MiddlewareFunc | string)[]) {
+    const { path, middlewares, options } = this.#formatRouteParams(nameOrPath, pathOrMiddleware, middleware);
+    if (typeof method === 'string') {
+      method = [ method ];
+    }
+    this.register(path, method, middlewares, options);
     return this;
   }
 
+  // const METHODS = [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete', 'all' ];
+  head(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  head(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  head(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: (MiddlewareFunc | string)[]): Router {
+    return this.verb('head', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+  options(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  options(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  options(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: (MiddlewareFunc | string)[]): Router {
+    return this.verb('options', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+  get(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  get(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  get(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: (MiddlewareFunc | string)[]): Router {
+    return this.verb('get', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+  put(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  put(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  put(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: (MiddlewareFunc | string)[]): Router {
+    return this.verb('put', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+  patch(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  patch(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  patch(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: (MiddlewareFunc | string)[]): Router {
+    return this.verb('patch', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+  post(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  post(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  post(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: (MiddlewareFunc | string)[]): Router {
+    return this.verb('post', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+  delete(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  delete(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  delete(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: (MiddlewareFunc | string)[]): Router {
+    return this.verb('delete', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+  all(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  all(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
+  all(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+    ...middlewares: (MiddlewareFunc | string)[]): Router {
+    return this.verb(methods, nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  register(path: string | string[] | RegExp | RegExp[],
+    methods: string[],
+    middleware: MiddlewareFunc | string | (MiddlewareFunc | string | ResourcesController)[],
+    opts?: RegisterOptions) {
+    // patch register to support generator function middleware and string controller
+    middleware = Array.isArray(middleware) ? middleware : [ middleware ];
+    const middlewares = convertMiddlewares(middleware, this.app);
+    return super.register(path, methods, middlewares, opts);
+  }
+
   /**
    * restful router api
    * @param {String} name - Router name
@@ -136,43 +207,34 @@ class EggRouter extends Router {
    * @return {Router} return route object.
    * @since 1.0.0
    */
-  resources(...args) {
-    const splited = spliteAndResolveRouterParams({ args, app: this.app });
-    const middlewares = splited.middlewares;
+  resources(prefix: string, controller: string | ResourcesController): Router;
+  resources(prefix: string, middleware: MiddlewareFunc, controller: string | ResourcesController): Router;
+  resources(name: string, prefix: string, controller: string | ResourcesController): Router;
+  resources(name: string, prefix: string, middleware: MiddlewareFunc, controller: string | ResourcesController): Router;
+  resources(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc | ResourcesController,
+    ...middleware: (MiddlewareFunc | string | ResourcesController)[]): Router {
+    const { path, middlewares, options } = this.#formatRouteParams(nameOrPath, pathOrMiddleware, middleware);
     // last argument is Controller object
-    const controller = splited.middlewares.pop();
-
-    let name = '';
-    let prefix = '';
-    if (splited.prefix.length === 2) {
-      // router.get('users', '/users')
-      name = splited.prefix[0];
-      prefix = splited.prefix[1];
-    } else {
-      // router.get('/users')
-      prefix = splited.prefix[0];
-    }
-
+    const controller = resolveController(middlewares.pop()!, this.app);
     for (const key in REST_MAP) {
-      const action = controller[key];
+      const action = controller[key] as MiddlewareFunc;
       if (!action) continue;
 
       const opts = REST_MAP[key];
-      let formatedName;
+      let routeName;
       if (opts.member) {
-        formatedName = inflection.singularize(name);
+        routeName = inflection.singularize(options.name ?? '');
       } else {
-        formatedName = inflection.pluralize(name);
+        routeName = inflection.pluralize(options.name ?? '');
       }
       if (opts.namePrefix) {
-        formatedName = opts.namePrefix + formatedName;
+        routeName = opts.namePrefix + routeName;
       }
-      prefix = prefix.replace(/\/$/, '');
-      const path = opts.suffix ? `${prefix}/${opts.suffix}` : prefix;
+      const prefix = (path as string).replace(/\/$/, '');
+      const urlPath = opts.suffix ? `${prefix}/${opts.suffix}` : prefix;
       const method = Array.isArray(opts.method) ? opts.method : [ opts.method ];
-      this.register(path, method, middlewares.concat(action), { name: formatedName });
+      this.register(urlPath, method, middlewares.concat(action), { name: routeName });
     }
-
     return this;
   }
 
@@ -189,23 +251,23 @@ class EggRouter extends Router {
    * @return {String} url by path name and query params.
    * @since 1.0.0
    */
-  url(name, params) {
+  url(name: string, params?: Record<string, string | number | (string | number)[]>): string {
     const route = this.route(name);
     if (!route) return '';
 
     const args = params;
     let url = route.path;
 
-    assert(!is.regExp(url), `Can't get the url for regExp ${url} for by name '${name}'`);
+    assert(!(url instanceof RegExp), `Can't get the url for regExp ${url} for by name '${name}'`);
 
     const queries = [];
     if (typeof args === 'object' && args !== null) {
-      const replacedParams = [];
-      url = url.replace(/:([a-zA-Z_]\w*)/g, function($0, key) {
-        if (utility.has(args, key)) {
+      const replacedParams: string[] = [];
+      url = url.replace(/:([a-zA-Z_]\w*)/g, ($0, key) => {
+        if (key in args) {
           const values = args[key];
           replacedParams.push(key);
-          return utility.encodeURIComponent(Array.isArray(values) ? values[0] : values);
+          return safeEncodeURIComponent(Array.isArray(values) ? String(values[0]) : String(values));
         }
         return $0;
       });
@@ -214,15 +276,14 @@ class EggRouter extends Router {
         if (replacedParams.includes(key)) {
           continue;
         }
-
         const values = args[key];
-        const encodedKey = utility.encodeURIComponent(key);
+        const encodedKey = safeEncodeURIComponent(key);
         if (Array.isArray(values)) {
           for (const val of values) {
-            queries.push(`${encodedKey}=${utility.encodeURIComponent(val)}`);
+            queries.push(`${encodedKey}=${safeEncodeURIComponent(String(val))}`);
           }
         } else {
-          queries.push(`${encodedKey}=${utility.encodeURIComponent(values)}`);
+          queries.push(`${encodedKey}=${safeEncodeURIComponent(String(values))}`);
         }
       }
     }
@@ -239,62 +300,33 @@ class EggRouter extends Router {
     return url;
   }
 
-  pathFor(name, params) {
+  /**
+   * @alias to url()
+   */
+  pathFor(name: string, params?: Record<string, string | number | (string | number)[]>) {
     return this.url(name, params);
   }
 }
 
-/**
- * 1. split (name, url, ...middleware, controller) to
- * {
- *   prefix: [name, url]
- *   middlewares [...middleware, controller]
- * }
- *
- * 2. resolve controller from string to function
- *
- * @param  {Object} options inputs
- * @param {Object} options.args router params
- * @param {Object} options.app egg application instance
- * @return {Object} prefix and middlewares
- */
-function spliteAndResolveRouterParams({ args, app }) {
-  let prefix;
-  let middlewares;
-  if (args.length >= 3 && (is.string(args[1]) || is.regExp(args[1]))) {
-    // app.get(name, url, [...middleware], controller)
-    prefix = args.slice(0, 2);
-    middlewares = args.slice(2);
-  } else {
-    // app.get(url, [...middleware], controller)
-    prefix = args.slice(0, 1);
-    middlewares = args.slice(1);
-  }
-  // resolve controller
-  const controller = middlewares.pop();
-  middlewares.push(resolveController(controller, app));
-  return { prefix, middlewares };
-}
-
 /**
  * resolve controller from string to function
- * @param  {String|Function} controller input controller
- * @param  {Application} app egg application instance
- * @return {Function} controller function
+ * @param {String|Function} controller input controller
+ * @param {Application} app egg application instance
  */
-function resolveController(controller, app) {
-  if (is.string(controller)) {
+function resolveController(controller: string | MiddlewareFunc | ResourcesController, app: Application) {
+  if (typeof controller === 'string') {
+    // resolveController('foo.bar.Home', app)
     const actions = controller.split('.');
     let obj = app.controller;
     actions.forEach(key => {
       obj = obj[key];
-      if (!obj) throw new Error(`controller '${controller}' not exists`);
+      if (!obj) throw new Error(`app.controller.${controller} not exists`);
     });
-    controller = obj;
+    controller = obj as any;
   }
   // ensure controller is exists
   if (!controller) throw new Error('controller not exists');
-  return controller;
+  return controller as any;
 }
 
 /**
@@ -309,17 +341,9 @@ function resolveController(controller, app) {
  *
  * @param  {Array} middlewares middlewares and controller(last middleware)
  * @param  {Application} app  egg application instance
- * @return {Array} middlewares
  */
-function convertMiddlewares(middlewares, app) {
+function convertMiddlewares(middlewares: (MiddlewareFunc | string | ResourcesController)[], app: Application) {
   // ensure controller is resolved
-  const controller = resolveController(middlewares.pop(), app);
-  // make middleware support generator function
-  middlewares = middlewares.map(utils.middleware);
-  const wrappedController = (ctx, next) => {
-    return utils.callFn(controller, [ ctx, next ], ctx);
-  };
-  return middlewares.concat([ wrappedController ]);
+  const controller = resolveController(middlewares.pop()!, app);
+  return [ ...middlewares as MiddlewareFunc[], controller ];
 }
-
-module.exports = EggRouter;
diff --git a/src/Layer.ts b/src/Layer.ts
index 8f936d0..2d4b4a1 100644
--- a/src/Layer.ts
+++ b/src/Layer.ts
@@ -2,6 +2,7 @@ import { debuglog } from 'node:util';
 import pathToRegExp, { type Key } from 'path-to-regexp';
 import URI from 'urijs';
 import { decodeURIComponent as safeDecodeURIComponent } from 'utility';
+import { isGeneratorFunction } from 'is-type-of';
 import type {
   MiddlewareFunc,
   MiddlewareFuncWithParamProperty,
@@ -31,7 +32,7 @@ export class Layer {
   readonly name?: string;
   readonly methods: string[] = [];
   readonly stack: MiddlewareFuncWithParamProperty[];
-  path: string;
+  path: string | RegExp;
   regexp: RegExp;
   paramNames: Key[] = [];
 
@@ -67,16 +68,21 @@ export class Layer {
 
     // ensure middleware is a function
     this.stack.forEach(fn => {
-      const type = (typeof fn);
+      const type = typeof fn;
       if (type !== 'function') {
         throw new TypeError(
           methods.toString() + ' `' + (this.opts.name || path) + '`: `middleware` '
           + 'must be a function, not `' + type + '`',
         );
       }
+      if (isGeneratorFunction(fn)) {
+        throw new TypeError(
+          methods.toString() + ' `' + (this.opts.name || path) + '`: Please use async function instead of generator function',
+        );
+      }
     });
 
-    this.path = typeof path === 'string' ? path : String(path);
+    this.path = path;
     this.regexp = pathToRegExp(path, this.paramNames, this.opts);
 
     debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
@@ -148,20 +154,25 @@ export class Layer {
    */
   url(params?: string | number | object, ...paramsOrOptions: (string | number | object | LayerURLOptions)[]): string {
     let args: Array<string | number | object> | object = params as object;
-    const url = this.path.replace(/\(\.\*\)/g, '');
+    const url = (this.path as string).replace(/\(\.\*\)/g, '');
     const toPath = pathToRegExp.compile(url);
     let options: LayerURLOptions | undefined;
 
     if (params !== undefined && typeof params !== 'object') {
       args = [ params, ...paramsOrOptions ];
-      // route.url(params1, params2, ..., options);
+      // route.url(stringOrNumber, params1, ..., options);
       if (Array.isArray(args)) {
         const lastIndex = args.length - 1;
         if (typeof args[lastIndex] === 'object') {
-          options = paramsOrOptions[lastIndex] as LayerURLOptions;
-          args = paramsOrOptions.slice(0, lastIndex);
+          options = args[lastIndex];
+          args = args.slice(0, lastIndex);
         }
       }
+    } else if (typeof params === 'object') {
+      if (typeof paramsOrOptions[0] === 'object' && 'query' in paramsOrOptions[0]) {
+        // route.url(param, options);
+        options = paramsOrOptions[0];
+      }
     }
 
     const tokens = pathToRegExp.parse(url);
diff --git a/src/Router.ts b/src/Router.ts
index 643d13e..b92527a 100644
--- a/src/Router.ts
+++ b/src/Router.ts
@@ -8,7 +8,7 @@ import compose from 'koa-compose';
 import HttpError from 'http-errors';
 import methods from 'methods';
 import { Layer, LayerURLOptions } from './Layer.js';
-import { MiddlewareFunc, MiddlewareFuncWithRouter, Next, ParamMiddlewareFunc } from './types.js';
+import { MiddlewareFunc, MiddlewareFuncWithRouter, Next, ParamMiddlewareFunc, ResourcesController } from './types.js';
 
 const debug = debuglog('egg-router:router');
 
@@ -391,13 +391,13 @@ export class Router {
   /**
    * Create and register a route.
    *
-   * @param {String|String[]} path Path string.
+   * @param {String|RegExp|(String|RegExp)[]} path Path string.
    * @param {String[]} methods Array of HTTP verbs.
    * @param {Function|Function[]} middleware Multiple middleware also accepted.
    * @param {Object} [opts] optional params
    * @private
    */
-  register(path: string | string[] | RegExp | RegExp[],
+  register(path: string | RegExp | (string | RegExp)[],
     methods: string[],
     middleware: MiddlewareFunc | MiddlewareFunc[],
     opts?: RegisterOptions): Layer | Layer[] {
@@ -593,6 +593,33 @@ export class Router {
     return this;
   }
 
+  protected _formatRouteParams(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc | ResourcesController,
+    middlewares: (MiddlewareFunc | string | ResourcesController)[]) {
+    const options: RegisterOptions = {};
+    let path: string | RegExp | (string | RegExp)[];
+    if (typeof pathOrMiddleware === 'string' || pathOrMiddleware instanceof RegExp) {
+      // verb(method, name, path, ...middlewares)
+      path = pathOrMiddleware;
+      assert(typeof nameOrPath === 'string', 'route name should be string');
+      options.name = nameOrPath;
+    } else if (Array.isArray(pathOrMiddleware)) {
+      // verb(method, name, paths, ...middlewares)
+      path = pathOrMiddleware;
+      assert(typeof nameOrPath === 'string', 'route name should be string');
+      options.name = nameOrPath;
+    } else {
+      // verb(method, path, ...middlewares)
+      path = nameOrPath;
+      middlewares = [ pathOrMiddleware, ...middlewares ];
+    }
+    return {
+      path,
+      middlewares,
+      options,
+    };
+  }
+
   /**
    * Create `router.verb()` methods, where *verb* is one of the HTTP verbs such
    * as `router.get()` or `router.post()`.
@@ -714,24 +741,15 @@ export class Router {
    * @param {Function} middlewares middlewares
    * @return {Router} Router instance
    */
-  verb(method: RouterMethod | RouterMethod[], nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
-    ...middlewares: MiddlewareFunc[]): Router {
-    const options: RegisterOptions = {};
-    let path: string | RegExp;
-    if (typeof pathOrMiddleware === 'string' || pathOrMiddleware instanceof RegExp) {
-      // verb(method, name, path, ...middlewares)
-      path = pathOrMiddleware;
-      assert(typeof nameOrPath === 'string', 'route name should be string');
-      options.name = nameOrPath;
-    } else {
-      // verb(method, path, ...middlewares)
-      path = nameOrPath;
-      middlewares = [ pathOrMiddleware, ...middlewares ];
-    }
+  verb(method: string | string[],
+    nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
+    ...middleware: MiddlewareFunc[]): Router {
+    const { options, path, middlewares } = this._formatRouteParams(nameOrPath, pathOrMiddleware, middleware);
     if (typeof method === 'string') {
       method = [ method ];
     }
-    this.register(path, method, middlewares, options);
+    this.register(path, method, middlewares as MiddlewareFunc[], options);
     return this;
   }
 
@@ -744,9 +762,10 @@ export class Router {
    * @return {Router} router instance
    * @private
    */
-  all(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  all(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  all(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  all(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  all(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  all(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb(methods, nameOrPath, pathOrMiddleware, ...middlewares);
   }
@@ -755,177 +774,211 @@ export class Router {
   // "m-search", "merge", "mkactivity", "mkcalendar", "mkcol", "move", "notify", "options",
   // "patch", "post", "propfind", "proppatch", "purge", "put", "rebind", "report", "search",
   // "source", "subscribe", "trace", "unbind", "unlink", "unlock", "unsubscribe"
-  acl(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  acl(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  acl(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  acl(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  acl(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  acl(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('acl', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  bind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  bind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  bind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  bind(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  bind(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  bind(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('bind', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  checkout(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  checkout(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  checkout(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  checkout(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  checkout(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  checkout(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('checkout', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  connect(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  connect(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  connect(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  connect(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  connect(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  connect(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('connect', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  copy(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  copy(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  copy(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  copy(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  copy(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  copy(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('copy', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  delete(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  delete(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  delete(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  delete(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  delete(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  delete(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('delete', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
   /** Alias for `router.delete()` because delete is a reserved word */
-  del(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  del(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  del(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  del(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  del(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  del(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('delete', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  get(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  get(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  get(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  get(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  get(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  get(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('get', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  head(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  head(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  head(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  query(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  query(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  query(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
+    return this.verb('query', nameOrPath, pathOrMiddleware, ...middlewares);
+  }
+
+  head(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  head(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  head(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('head', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  link(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  link(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  link(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  link(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  link(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  link(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('link', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  lock(path: string, ...middlewares: MiddlewareFunc[]): Router;
-  lock(name: string, path: string, ...middlewares: MiddlewareFunc[]): Router;
-  lock(nameOrPath: string, pathOrMiddleware: string | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router {
+  lock(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  lock(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  lock(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
+    ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('lock', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  ['m-search'](path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  ['m-search'](name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  ['m-search'](nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  ['m-search'](path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  ['m-search'](name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  ['m-search'](nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('m-search', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  merge(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  merge(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  merge(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  merge(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  merge(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  merge(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('merge', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  mkactivity(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  mkactivity(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  mkactivity(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  mkactivity(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  mkactivity(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  mkactivity(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('mkactivity', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  mkcalendar(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  mkcalendar(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  mkcalendar(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  mkcalendar(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  mkcalendar(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  mkcalendar(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('mkcalendar', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  mkcol(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  mkcol(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  mkcol(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  mkcol(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  mkcol(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  mkcol(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('mkcol', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  move(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  move(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  move(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  move(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  move(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  move(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('move', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  notify(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  notify(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  notify(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  notify(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  notify(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  notify(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('notify', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  options(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  options(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  options(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  options(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  options(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  options(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('options', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  patch(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  patch(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  patch(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  patch(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  patch(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  patch(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('patch', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  post(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  post(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  post(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  post(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  post(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  post(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('post', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  propfind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  propfind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  propfind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  propfind(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  propfind(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  propfind(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('propfind', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  proppatch(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  proppatch(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  proppatch(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  proppatch(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  proppatch(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  proppatch(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('proppatch', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  purge(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  purge(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  purge(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  purge(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  purge(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  purge(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('purge', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  put(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  put(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  put(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  put(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  put(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  put(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('put', nameOrPath, pathOrMiddleware, ...middlewares);
   }
@@ -937,65 +990,74 @@ export class Router {
     return this.verb('rebind', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  report(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  report(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  report(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  report(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  report(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  report(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('report', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  search(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  search(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  search(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  search(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  search(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  search(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('search', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  source(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  source(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  source(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  source(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  source(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  source(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('source', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  subscribe(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  subscribe(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  subscribe(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  subscribe(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  subscribe(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  subscribe(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('subscribe', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  trace(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  trace(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  trace(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  trace(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  trace(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  trace(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('trace', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  unbind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  unbind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  unbind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  unbind(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  unbind(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  unbind(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('unbind', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  unlink(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  unlink(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  unlink(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  unlink(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  unlink(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  unlink(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('unlink', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  unlock(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  unlock(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  unlock(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  unlock(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  unlock(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  unlock(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('unlock', nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  unsubscribe(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  unsubscribe(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router;
-  unsubscribe(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  unsubscribe(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  unsubscribe(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router;
+  unsubscribe(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: MiddlewareFunc[]): Router {
     return this.verb('unsubscribe', nameOrPath, pathOrMiddleware, ...middlewares);
   }
diff --git a/src/index.ts b/src/index.ts
index 5e3332e..37add7a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,7 +1,7 @@
 import { Router } from './Router.js';
 
 export * from './Router.js';
-// import EggRouter from './lib/egg_router';
+export * from './EggRouter.js';
 
 export const KoaRouter = Router;
 export default Router;
diff --git a/src/types.ts b/src/types.ts
index b0311c0..f606fe7 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -3,3 +3,13 @@ export type MiddlewareFunc = (ctx: any, next: Next) => Promise<void> | void;
 export type MiddlewareFuncWithParamProperty = MiddlewareFunc & { param?: string };
 export type ParamMiddlewareFunc = (param: string, ctx: any, next: Next) => Promise<void> | void;
 export type MiddlewareFuncWithRouter<T> = MiddlewareFunc & { router: T };
+
+export interface ResourcesController {
+  index?: MiddlewareFunc;
+  new?: MiddlewareFunc;
+  create?: MiddlewareFunc;
+  show?: MiddlewareFunc;
+  edit?: MiddlewareFunc;
+  update?: MiddlewareFunc;
+  destroy?: MiddlewareFunc;
+}
diff --git a/src/utils.ts b/src/utils.ts
deleted file mode 100644
index 8348173..0000000
--- a/src/utils.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { isFunction, isGeneratorFunction } from 'is-type-of';
-import { MiddlewareFunc } from './types.js';
-
-type Fn = (...args: any[]) => any;
-
-export async function callFn(fn: Fn, args: any[], ctx: unknown) {
-  args = args || [];
-  if (!isFunction(fn)) {
-    return;
-  }
-  if (isGeneratorFunction(fn)) {
-    throw new TypeError(`Please use async function instead of generator function: ${fn.toString()}`);
-  }
-  return ctx ? fn.call(ctx, ...args) : fn(...args);
-}
-
-export function middleware(fn: MiddlewareFunc) {
-  if (isGeneratorFunction(fn)) {
-    throw new TypeError(`Please use async function instead of generator function: ${fn.toString()}`);
-  }
-  return fn;
-}
diff --git a/test/EggRouter.test.ts b/test/EggRouter.test.ts
index 8a729de..1dec08e 100644
--- a/test/EggRouter.test.ts
+++ b/test/EggRouter.test.ts
@@ -1,20 +1,18 @@
-'use strict';
+import { strict as assert } from 'node:assert';
+import is from 'is-type-of';
+import { EggRouter } from '../src/index.js';
 
-const EggRouter = require('../dist/commonjs').EggRouter;
-const assert = require('assert');
-const is = require('is-type-of');
-
-describe('test/lib/egg_router.test.js', () => {
-  it('creates new router with egg app', function() {
+describe('test/EggRouter.test.ts', () => {
+  it('creates new router with egg app', () => {
     const app = { controller: {} };
     const router = new EggRouter({}, app);
     assert(router);
     [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete', 'all', 'resources' ].forEach(method => {
-      assert(typeof router[method] === 'function');
+      assert.equal(typeof Reflect.get(router, method), 'function');
     });
   });
 
-  it('should app.verb(url, controller) work', () => {
+  it('should throw error on generator function', () => {
     const app = {
       controller: {
         async foo() { return; },
@@ -24,6 +22,27 @@ describe('test/lib/egg_router.test.js', () => {
       },
     };
 
+    const router = new EggRouter({}, app);
+    router.get('/foo', app.controller.foo);
+    assert.throws(() => {
+      router.post('/hello/world', app.controller.hello.world as any);
+    }, (err: TypeError) => {
+      assert(err instanceof TypeError);
+      assert.equal(err.message, 'post `/hello/world`: Please use async function instead of generator function');
+      return true;
+    });
+  });
+
+  it('should app.verb(url, controller) work', () => {
+    const app = {
+      controller: {
+        async foo() { return; },
+        hello: {
+          world() { return; },
+        },
+      },
+    };
+
     const router = new EggRouter({}, app);
     router.get('/foo', app.controller.foo);
     router.post('/hello/world', app.controller.hello.world);
@@ -41,7 +60,7 @@ describe('test/lib/egg_router.test.js', () => {
       controller: {
         async foo() { return; },
         hello: {
-          * world() { return; },
+          world() { return; },
         },
       },
     };
@@ -65,7 +84,7 @@ describe('test/lib/egg_router.test.js', () => {
       controller: {
         async foo() { return; },
         hello: {
-          * world() { return; },
+          world() { return; },
         },
       },
     };
@@ -89,7 +108,7 @@ describe('test/lib/egg_router.test.js', () => {
       controller: {
         async foo() { return; },
         hello: {
-          * world() { return; },
+          world() { return; },
         },
       },
     };
@@ -97,10 +116,10 @@ describe('test/lib/egg_router.test.js', () => {
     const router = new EggRouter({}, app);
     assert.throws(() => {
       router.get('foo', '/foo', 'foobar');
-    }, /controller 'foobar' not exists/);
+    }, /app.controller.foobar not exists/);
 
     assert.throws(() => {
-      router.get('/foo', app.bar);
+      router.get('/foo', (app as any).bar);
     }, /controller not exists/);
   });
 
@@ -109,18 +128,20 @@ describe('test/lib/egg_router.test.js', () => {
       controller: {
         async foo() { return; },
         hello: {
-          * world() { return; },
+          world() { return; },
         },
       },
     };
 
-    const generatorMiddleware = function* () { return; };
+    const asyncMiddleware1 = async function() { return; };
     const asyncMiddleware = async function() { return; };
     const commonMiddleware = function() {};
 
     const router = new EggRouter({}, app);
-    router.get('foo', '/foo', generatorMiddleware, asyncMiddleware, commonMiddleware, 'foo');
-    router.post('hello', '/hello/world', generatorMiddleware, asyncMiddleware, commonMiddleware, 'hello.world');
+    router.get('foo', '/foo', asyncMiddleware1, asyncMiddleware, commonMiddleware, 'foo');
+    router.post('hello', '/hello/world', asyncMiddleware1, asyncMiddleware, commonMiddleware, 'hello.world');
+    router.get('foo', '/foo', asyncMiddleware1, asyncMiddleware, commonMiddleware, 'foo');
+    router.post('hello', '/hello/world', asyncMiddleware1, asyncMiddleware, commonMiddleware, 'hello.world');
 
     assert(router.stack[0].name === 'foo');
     assert(router.stack[0].path === '/foo');
@@ -155,13 +176,13 @@ describe('test/lib/egg_router.test.js', () => {
 
     const router = new EggRouter({}, app);
     router.resources('/post', asyncMiddleware, app.controller.post);
-    assert(router.stack.length === 5);
-    assert(router.stack[0].stack.length === 2);
+    assert.equal(router.stack.length, 5);
+    assert.equal(router.stack[0].stack.length, 2);
 
     router.resources('api_post', '/api/post', app.controller.post);
-    assert(router.stack.length === 10);
-    assert(router.stack[5].stack.length === 1);
-    assert(router.stack[5].name === 'api_posts');
+    assert.equal(router.stack.length, 10);
+    assert.equal(router.stack[5].stack.length, 1);
+    assert.equal(router.stack[5].name, 'api_posts');
   });
 
   it('should router.url work', () => {
@@ -169,7 +190,7 @@ describe('test/lib/egg_router.test.js', () => {
       controller: {
         async foo() { return; },
         hello: {
-          * world() { return; },
+          world() { return; },
         },
       },
     };
@@ -177,13 +198,13 @@ describe('test/lib/egg_router.test.js', () => {
     router.get('post', '/post/:id', app.controller.foo);
     router.get('hello', '/hello/world', app.controller.hello.world);
 
-    assert(router.url('post', { id: 1, foo: [ 1, 2 ], bar: 'bar' }) === '/post/1?foo=1&foo=2&bar=bar');
-    assert(router.url('post', { foo: [ 1, 2 ], bar: 'bar' }) === '/post/:id?foo=1&foo=2&bar=bar');
-    assert(router.url('fooo') === '');
-    assert(router.url('hello') === '/hello/world');
+    assert.equal(router.url('post', { id: 1, foo: [ 1, 2 ], bar: 'bar' }), '/post/1?foo=1&foo=2&bar=bar');
+    assert.equal(router.url('post', { foo: [ 1, 2 ], bar: 'bar' }), '/post/:id?foo=1&foo=2&bar=bar');
+    assert.equal(router.url('fooo'), '');
+    assert.equal(router.url('hello'), '/hello/world');
 
-    assert(router.pathFor('post', { id: 1, foo: [ 1, 2 ], bar: 'bar' }) === '/post/1?foo=1&foo=2&bar=bar');
-    assert(router.pathFor('fooo') === '');
-    assert(router.pathFor('hello') === '/hello/world');
+    assert.equal(router.pathFor('post', { id: 1, foo: [ 1, 2 ], bar: 'bar' }), '/post/1?foo=1&foo=2&bar=bar');
+    assert.equal(router.pathFor('fooo'), '');
+    assert.equal(router.pathFor('hello'), '/hello/world');
   });
 });
diff --git a/test/Router.test.ts b/test/Router.test.ts
index ab0bfe8..d32f68d 100644
--- a/test/Router.test.ts
+++ b/test/Router.test.ts
@@ -1,24 +1,18 @@
-'use strict';
-
-const fs = require('fs');
-const http = require('http');
-const Koa = require('koa');
-const methods = require('methods');
-const path = require('path');
-const request = require('supertest');
-const Router = require('../../lib/router');
-const Layer = require('../../lib/layer');
-const expect = require('expect.js');
-const should = require('should');
-
-describe('test/lib/router.test.js', function() {
-  it('creates new router', function(done) {
+import { strict as assert } from 'node:assert';
+import fs from 'node:fs';
+import Koa from '@eggjs/koa';
+import methods from 'methods';
+import request from 'supertest';
+import Router from '../src/index.js';
+import { Next } from '../src/types.js';
+
+describe('test/lib/router.test.js', () => {
+  it('creates new router', () => {
     const router = new Router();
-    router.should.be.instanceOf(Router);
-    done();
+    assert(router instanceof Router);
   });
 
-  it('shares context between routers (gh-205)', function(done) {
+  it('shares context between routers (gh-205)', async () => {
     const app = new Koa();
     const router1 = new Router();
     const router2 = new Router();
@@ -32,17 +26,13 @@ describe('test/lib/router.test.js', function() {
       return next();
     });
     app.use(router1.routes()).use(router2.routes());
-    request(http.createServer(app.callback()))
+    const res = await request(app.callback())
       .get('/')
-      .expect(200)
-      .end(function(err, res) {
-        if (err) return done(err);
-        expect(res.body).to.have.property('foo', 'bar');
-        done();
-      });
+      .expect(200);
+    assert.equal(res.body.foo, 'bar');
   });
 
-  it('does not register middleware more than once (gh-184)', function(done) {
+  it('does not register middleware more than once (gh-184)', async () => {
     const app = new Koa();
     const parentRouter = new Router();
     const nestedRouter = new Router();
@@ -51,10 +41,10 @@ describe('test/lib/router.test.js', function() {
       .get('/first-nested-route', function(ctx) {
         ctx.body = { n: ctx.n };
       })
-      .get('/second-nested-route', function(ctx, next) {
+      .get('/second-nested-route', function(_ctx, next) {
         return next();
       })
-      .get('/third-nested-route', function(ctx, next) {
+      .get('/third-nested-route', function(_ctx, next) {
         return next();
       });
 
@@ -65,17 +55,13 @@ describe('test/lib/router.test.js', function() {
 
     app.use(parentRouter.routes());
 
-    request(http.createServer(app.callback()))
+    const res = await request(app.callback())
       .get('/parent-route/first-nested-route')
-      .expect(200)
-      .end(function(err, res) {
-        if (err) return done(err);
-        expect(res.body).to.have.property('n', 1);
-        done();
-      });
+      .expect(200);
+    assert.equal(res.body.n, 1);
   });
 
-  it('router can be accecced with ctx', function(done) {
+  it('router can be access with ctx', async () => {
     const app = new Koa();
     const router = new Router();
     router.get('home', '/', function(ctx) {
@@ -84,17 +70,13 @@ describe('test/lib/router.test.js', function() {
       };
     });
     app.use(router.routes());
-    request(http.createServer(app.callback()))
+    const res = await request(app.callback())
       .get('/')
-      .expect(200)
-      .end(function(err, res) {
-        if (err) return done(err);
-        expect(res.body.url).to.eql('/');
-        done();
-      });
+      .expect(200);
+    assert.equal(res.body.url, '/');
   });
 
-  it('registers multiple middleware for one route', function(done) {
+  it('registers multiple middleware for one route', async () => {
     const app = new Koa();
     const router = new Router();
 
@@ -118,54 +100,45 @@ describe('test/lib/router.test.js', function() {
 
     app.use(router.routes());
 
-    request(http.createServer(app.callback()))
+    const res = await request(app.callback())
       .get('/double')
-      .expect(200)
-      .end(function(err, res) {
-        if (err) return done(err);
-        expect(res.body.message).to.eql('Hello World!');
-        done();
-      });
+      .expect(200);
+    assert.equal(res.body.message, 'Hello World!');
   });
 
-  it('does not break when nested-routes use regexp paths', function(done) {
+  it('does not break when nested-routes use regexp paths', () => {
     const app = new Koa();
     const parentRouter = new Router();
     const nestedRouter = new Router();
 
     nestedRouter
-      .get(/^\/\w$/i, function(ctx, next) {
+      .get(/^\/\w$/i, function(_ctx, next) {
         return next();
       })
-      .get('/first-nested-route', function(ctx, next) {
+      .get('/first-nested-route', function(_ctx, next) {
         return next();
       })
-      .get('/second-nested-route', function(ctx, next) {
+      .get('/second-nested-route', function(_ctx, next) {
         return next();
       });
 
-    parentRouter.use('/parent-route', function(ctx, next) {
+    parentRouter.use('/parent-route', function(_ctx, next) {
       return next();
     }, nestedRouter.routes());
 
     app.use(parentRouter.routes());
-    app.should.be.ok;
-    done();
+    assert(app);
   });
 
-  it('exposes middleware factory', function(done) {
+  it('exposes middleware factory', () => {
     const router = new Router();
-    router.should.have.property('routes');
-    router.routes.should.be.type('function');
+    assert.equal(typeof router.routes, 'function');
     const middleware = router.routes();
-    should.exist(middleware);
-    middleware.should.be.type('function');
-    done();
+    assert.equal(typeof middleware, 'function');
   });
 
-  it('supports promises for async/await', function(done) {
+  it('supports promises for async/await', async () => {
     const app = new Koa();
-    app.experimental = true;
     const router = new Router();
     router.get('/async', function(ctx) {
       return new Promise(function(resolve) {
@@ -179,17 +152,13 @@ describe('test/lib/router.test.js', function() {
     });
 
     app.use(router.routes()).use(router.allowedMethods());
-    request(http.createServer(app.callback()))
+    const res = await request(app.callback())
       .get('/async')
-      .expect(200)
-      .end(function(err, res) {
-        if (err) return done(err);
-        expect(res.body).to.have.property('msg', 'promises!');
-        done();
-      });
+      .expect(200);
+    assert.equal(res.body.msg, 'promises!');
   });
 
-  it('matches middleware only if route was matched (gh-182)', function(done) {
+  it('matches middleware only if route was matched (gh-182)', async () => {
     const app = new Koa();
     const router = new Router();
     const otherRouter = new Router();
@@ -205,18 +174,14 @@ describe('test/lib/router.test.js', function() {
 
     app.use(router.routes()).use(otherRouter.routes());
 
-    request(http.createServer(app.callback()))
+    const res = await request(app.callback())
       .get('/bar')
-      .expect(200)
-      .end(function(err, res) {
-        if (err) return done(err);
-        expect(res.body).to.have.property('foo', 'bar');
-        expect(res.body).to.not.have.property('bar');
-        done();
-      });
+      .expect(200);
+    assert.equal(res.body.foo, 'bar');
+    assert.equal(res.body.bar, undefined);
   });
 
-  it('matches first to last', function(done) {
+  it('matches first to last', async () => {
     const app = new Koa();
     const router = new Router();
 
@@ -231,17 +196,13 @@ describe('test/lib/router.test.js', function() {
         ctx.body = { order: 3 };
       });
 
-    request(http.createServer(app.use(router.routes()).callback()))
+    const res = await request(app.use(router.routes()).callback())
       .get('/user/account.jsx')
-      .expect(200)
-      .end(function(err, res) {
-        if (err) return done(err);
-        expect(res.body).to.have.property('order', 1);
-        done();
-      });
+      .expect(200);
+    assert.equal(res.body.order, 1);
   });
 
-  it('does not run subsequent middleware without calling next', function(done) {
+  it('does not run subsequent middleware without calling next', async () => {
     const app = new Koa();
     const router = new Router();
 
@@ -252,13 +213,12 @@ describe('test/lib/router.test.js', function() {
         ctx.body = { order: 1 };
       });
 
-    request(http.createServer(app.use(router.routes()).callback()))
+    await request(app.use(router.routes()).callback())
       .get('/user/account.jsx')
-      .expect(404)
-      .end(done);
+      .expect(404);
   });
 
-  it('nests routers with prefixes at root', function(done) {
+  it('nests routers with prefixes at root', async () => {
     const app = new Koa();
     const forums = new Router({
       prefix: '/forums',
@@ -279,35 +239,22 @@ describe('test/lib/router.test.js', function() {
 
     forums.use(posts.routes());
 
-    const server = http.createServer(app.use(forums.routes()).callback());
+    const server = app.use(forums.routes()).callback();
 
-    request(server)
+    await request(server)
       .get('/forums/1/posts')
-      .expect(204)
-      .end(function(err) {
-        if (err) return done(err);
-
-        request(server)
-          .get('/forums/1')
-          .expect(404)
-          .end(function(err) {
-            if (err) return done(err);
-
-            request(server)
-              .get('/forums/1/posts/2')
-              .expect(200)
-              .end(function(err, res) {
-                if (err) return done(err);
-
-                expect(res.body).to.have.property('fid', '1');
-                expect(res.body).to.have.property('pid', '2');
-                done();
-              });
-          });
-      });
+      .expect(204);
+    await request(server)
+      .get('/forums/1')
+      .expect(404);
+    const res = await request(server)
+      .get('/forums/1/posts/2')
+      .expect(200);
+    assert.equal(res.body.fid, '1');
+    assert.equal(res.body.pid, '2');
   });
 
-  it('nests routers with prefixes at path', function(done) {
+  it('nests routers with prefixes at path', async () => {
     const app = new Koa();
     const forums = new Router({
       prefix: '/api',
@@ -328,35 +275,24 @@ describe('test/lib/router.test.js', function() {
 
     forums.use('/forums/:fid', posts.routes());
 
-    const server = http.createServer(app.use(forums.routes()).callback());
+    const server = app.use(forums.routes()).callback();
 
-    request(server)
+    await request(server)
       .get('/api/forums/1/posts')
-      .expect(204)
-      .end(function(err) {
-        if (err) return done(err);
-
-        request(server)
-          .get('/api/forums/1')
-          .expect(404)
-          .end(function(err) {
-            if (err) return done(err);
-
-            request(server)
-              .get('/api/forums/1/posts/2')
-              .expect(200)
-              .end(function(err, res) {
-                if (err) return done(err);
-
-                expect(res.body).to.have.property('fid', '1');
-                expect(res.body).to.have.property('pid', '2');
-                done();
-              });
-          });
-      });
+      .expect(204);
+
+    await request(server)
+      .get('/api/forums/1')
+      .expect(404);
+
+    const res = await request(server)
+      .get('/api/forums/1/posts/2')
+      .expect(200);
+    assert.equal(res.body.fid, '1');
+    assert.equal(res.body.pid, '2');
   });
 
-  it('runs subrouter middleware after parent', function(done) {
+  it('runs subrouter middleware after parent', async () => {
     const app = new Koa();
     const subrouter = new Router()
       .use(function(ctx, next) {
@@ -372,17 +308,13 @@ describe('test/lib/router.test.js', function() {
         return next();
       })
       .use(subrouter.routes());
-    request(http.createServer(app.use(router.routes()).callback()))
+    const res = await request(app.use(router.routes()).callback())
       .get('/')
-      .expect(200)
-      .end(function(err, res) {
-        if (err) return done(err);
-        expect(res.body).to.have.property('msg', 'subrouter');
-        done();
-      });
+      .expect(200);
+    assert.equal(res.body.msg, 'subrouter');
   });
 
-  it('runs parent middleware for subrouter routes', function(done) {
+  it('runs parent middleware for subrouter routes', async () => {
     const app = new Koa();
     const subrouter = new Router()
       .get('/sub', function(ctx) {
@@ -394,59 +326,43 @@ describe('test/lib/router.test.js', function() {
         return next();
       })
       .use('/parent', subrouter.routes());
-    request(http.createServer(app.use(router.routes()).callback()))
+    const res = await request(app.use(router.routes()).callback())
       .get('/parent/sub')
-      .expect(200)
-      .end(function(err, res) {
-        if (err) return done(err);
-        expect(res.body).to.have.property('msg', 'router');
-        done();
-      });
+      .expect(200);
+    assert.equal(res.body.msg, 'router');
   });
 
-  it('matches corresponding requests', function(done) {
+  it('matches corresponding requests', async () => {
     const app = new Koa();
     const router = new Router();
     app.use(router.routes());
     router.get('/:category/:title', function(ctx) {
-      ctx.should.have.property('params');
-      ctx.params.should.have.property('category', 'programming');
-      ctx.params.should.have.property('title', 'how-to-node');
+      assert.equal(ctx.params.category, 'programming');
+      assert.equal(ctx.params.title, 'how-to-node');
       ctx.status = 204;
     });
     router.post('/:category', function(ctx) {
-      ctx.should.have.property('params');
-      ctx.params.should.have.property('category', 'programming');
+      assert.equal(ctx.params.category, 'programming');
       ctx.status = 204;
     });
     router.put('/:category/not-a-title', function(ctx) {
-      ctx.should.have.property('params');
-      ctx.params.should.have.property('category', 'programming');
-      ctx.params.should.not.have.property('title');
+      assert.equal(ctx.params.category, 'programming');
+      assert.equal(ctx.params.title, undefined);
       ctx.status = 204;
     });
-    const server = http.createServer(app.callback());
-    request(server)
+    const server = app.callback();
+    await request(server)
       .get('/programming/how-to-node')
-      .expect(204)
-      .end(function(err) {
-        if (err) return done(err);
-        request(server)
-          .post('/programming')
-          .expect(204)
-          .end(function(err) {
-            if (err) return done(err);
-            request(server)
-              .put('/programming/not-a-title')
-              .expect(204)
-              .end(function(err) {
-                done(err);
-              });
-          });
-      });
+      .expect(204);
+    await request(server)
+      .post('/programming')
+      .expect(204);
+    await request(server)
+      .put('/programming/not-a-title')
+      .expect(204);
   });
 
-  it('executes route middleware using `app.context`', function(done) {
+  it('executes route middleware using `app.context`', async () => {
     const app = new Koa();
     const router = new Router();
     app.use(router.routes());
@@ -458,23 +374,19 @@ describe('test/lib/router.test.js', function() {
       ctx.foo = 'bar';
       return next();
     }, function(ctx) {
-      ctx.should.have.property('bar', 'baz');
-      ctx.should.have.property('foo', 'bar');
-      ctx.should.have.property('app');
-      ctx.should.have.property('req');
-      ctx.should.have.property('res');
-      ctx.status = 204;
-      done();
+      ctx.body = {
+        bar: ctx.bar,
+        foo: ctx.foo,
+      };
     });
-    request(http.createServer(app.callback()))
+    const res = await request(app.callback())
       .get('/match/this')
-      .expect(204)
-      .end(function(err) {
-        if (err) return done(err);
-      });
+      .expect(200);
+    assert.equal(res.body.bar, 'baz');
+    assert.equal(res.body.foo, 'bar');
   });
 
-  it('does not match after ctx.throw()', function(done) {
+  it('does not match after ctx.throw()', async () => {
     const app = new Koa();
     let counter = 0;
     const router = new Router();
@@ -486,64 +398,53 @@ describe('test/lib/router.test.js', function() {
     router.get('/', function() {
       counter++;
     });
-    const server = http.createServer(app.callback());
-    request(server)
+    await request(app.callback())
       .get('/')
-      .expect(403)
-      .end(function(err) {
-        if (err) return done(err);
-        counter.should.equal(1);
-        done();
-      });
+      .expect(403);
+    assert.equal(counter, 1);
   });
 
-  it('supports promises for route middleware', function(done) {
+  it('supports promises for route middleware', async () => {
     const app = new Koa();
     const router = new Router();
     app.use(router.routes());
     const readVersion = function() {
       return new Promise(function(resolve, reject) {
-        const packagePath = path.join(__dirname, '..', '..', 'package.json');
-        fs.readFile(packagePath, 'utf8', function(err, data) {
+        fs.readFile('package.json', 'utf8', function(err, data) {
           if (err) return reject(err);
           resolve(JSON.parse(data).version);
         });
       });
     };
     router
-      .get('/', function(ctx, next) {
+      .get('/', function(_ctx, next) {
         return next();
       }, function(ctx) {
         return readVersion().then(function() {
           ctx.status = 204;
         });
       });
-    request(http.createServer(app.callback()))
+    await request(app.callback())
       .get('/')
-      .expect(204)
-      .end(done);
+      .expect(204);
   });
 
-  describe('Router#allowedMethods()', function() {
-    it('responds to OPTIONS requests', function(done) {
+  describe('Router#allowedMethods()', () => {
+    it('responds to OPTIONS requests', async () => {
       const app = new Koa();
       const router = new Router();
       app.use(router.routes());
       app.use(router.allowedMethods());
       router.get('/users', function() {});
       router.put('/users', function() {});
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .options('/users')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          res.header.should.have.property('content-length', '0');
-          res.header.should.have.property('allow', 'HEAD, GET, PUT');
-          done();
-        });
+        .expect(200);
+      assert.equal(res.headers['content-length'], '0');
+      assert.equal(res.headers.allow, 'HEAD, GET, PUT');
     });
 
-    it('responds with 405 Method Not Allowed', function(done) {
+    it('responds with 405 Method Not Allowed', async () => {
       const app = new Koa();
       const router = new Router();
       router.get('/users', function() {});
@@ -551,17 +452,13 @@ describe('test/lib/router.test.js', function() {
       router.post('/events', function() {});
       app.use(router.routes());
       app.use(router.allowedMethods());
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .post('/users')
-        .expect(405)
-        .end(function(err, res) {
-          if (err) return done(err);
-          res.header.should.have.property('allow', 'HEAD, GET, PUT');
-          done();
-        });
+        .expect(405);
+      assert.equal(res.headers.allow, 'HEAD, GET, PUT');
     });
 
-    it('responds ignore allowedMethods when status is already set', function(done) {
+    it('responds ignore allowedMethods when status is already set', async () => {
       const app = new Koa();
       const router = new Router();
       router.get('/users', function() {});
@@ -573,24 +470,21 @@ describe('test/lib/router.test.js', function() {
       });
       app.use(router.routes());
       app.use(router.allowedMethods());
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .post('/users')
-        .expect(200)
-        .end(function(err) {
-          if (err) return done(err);
-          done();
-        });
+        .expect(200);
+      assert.equal(res.headers.allow, undefined);
     });
 
-    it('responds with 405 Method Not Allowed using the "throw" option', function(done) {
+    it('responds with 405 Method Not Allowed using the "throw" option', async () => {
       const app = new Koa();
       const router = new Router();
       app.use(router.routes());
       app.use(function(ctx, next) {
         return next().catch(function(err) {
           // assert that the correct HTTPError was thrown
-          err.name.should.equal('MethodNotAllowedError');
-          err.statusCode.should.equal(405);
+          // err.name.should.equal('MethodNotAllowedError');
+          // err.statusCode.should.equal(405);
 
           // translate the HTTPError to a normal response
           ctx.body = err.name;
@@ -601,26 +495,22 @@ describe('test/lib/router.test.js', function() {
       router.get('/users', function() {});
       router.put('/users', function() {});
       router.post('/events', function() {});
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .post('/users')
-        .expect(405)
-        .end(function(err, res) {
-          if (err) return done(err);
-          // the 'Allow' header is not set when throwing
-          res.header.should.not.have.property('allow');
-          done();
-        });
+        .expect(405);
+      // the 'Allow' header is not set when throwing
+      assert.equal(res.headers.allow, undefined);
     });
 
-    it('responds with user-provided throwable using the "throw" and "methodNotAllowed" options', function(done) {
+    it('responds with user-provided throwable using the "throw" and "methodNotAllowed" options', async () => {
       const app = new Koa();
       const router = new Router();
       app.use(router.routes());
       app.use(function(ctx, next) {
         return next().catch(function(err) {
           // assert that the correct HTTPError was thrown
-          err.message.should.equal('Custom Not Allowed Error');
-          err.statusCode.should.equal(405);
+          // err.message.should.equal('Custom Not Allowed Error');
+          // err.statusCode.should.equal(405);
 
           // translate the HTTPError to a normal response
           ctx.body = err.body;
@@ -630,7 +520,7 @@ describe('test/lib/router.test.js', function() {
       app.use(router.allowedMethods({
         throw: true,
         methodNotAllowed() {
-          const notAllowedErr = new Error('Custom Not Allowed Error');
+          const notAllowedErr: any = new Error('Custom Not Allowed Error');
           notAllowedErr.type = 'custom';
           notAllowedErr.statusCode = 405;
           notAllowedErr.body = {
@@ -644,46 +534,39 @@ describe('test/lib/router.test.js', function() {
       router.get('/users', function() {});
       router.put('/users', function() {});
       router.post('/events', function() {});
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .post('/users')
-        .expect(405)
-        .end(function(err, res) {
-          if (err) return done(err);
-          // the 'Allow' header is not set when throwing
-          res.header.should.not.have.property('allow');
-          res.body.should.eql({ error: 'Custom Not Allowed Error',
-            statusCode: 405,
-            otherStuff: true,
-          });
-          done();
-        });
+        .expect(405);
+      // the 'Allow' header is not set when throwing
+      assert.equal(res.headers.allow, undefined);
+      assert.deepEqual(res.body, {
+        error: 'Custom Not Allowed Error',
+        statusCode: 405,
+        otherStuff: true,
+      });
     });
 
-    it('responds with 501 Not Implemented', function(done) {
+    it('responds with 501 Not Implemented', async () => {
       const app = new Koa();
       const router = new Router();
       app.use(router.routes());
       app.use(router.allowedMethods());
       router.get('/users', function() {});
       router.put('/users', function() {});
-      request(http.createServer(app.callback()))
+      await request(app.callback())
         .search('/users')
-        .expect(501)
-        .end(function(err) {
-          if (err) return done(err);
-          done();
-        });
+        .expect(501);
     });
 
-    it('responds with 501 Not Implemented using the "throw" option', function(done) {
+    it('responds with 501 Not Implemented using the "throw" option', async () => {
       const app = new Koa();
       const router = new Router();
       app.use(router.routes());
       app.use(function(ctx, next) {
         return next().catch(function(err) {
           // assert that the correct HTTPError was thrown
-          err.name.should.equal('NotImplementedError');
-          err.statusCode.should.equal(501);
+          // err.name.should.equal('NotImplementedError');
+          // err.statusCode.should.equal(501);
 
           // translate the HTTPError to a normal response
           ctx.body = err.name;
@@ -693,27 +576,23 @@ describe('test/lib/router.test.js', function() {
       app.use(router.allowedMethods({ throw: true }));
       router.get('/users', function() {});
       router.put('/users', function() {});
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .search('/users')
-        .expect(501)
-        .end(function(err, res) {
-          if (err) return done(err);
-          // the 'Allow' header is not set when throwing
-          res.header.should.not.have.property('allow');
-          done();
-        });
+        .expect(501);
+      // the 'Allow' header is not set when throwing
+      assert.equal(res.headers.allow, undefined);
     });
 
-    it('responds with user-provided throwable using the "throw" and "notImplemented" options', function(done) {
+    it('responds with user-provided throwable using the "throw" and "notImplemented" options', async () => {
       const app = new Koa();
       const router = new Router();
       app.use(router.routes());
       app.use(function(ctx, next) {
         return next().catch(function(err) {
           // assert that our custom error was thrown
-          err.message.should.equal('Custom Not Implemented Error');
-          err.type.should.equal('custom');
-          err.statusCode.should.equal(501);
+          // err.message.should.equal('Custom Not Implemented Error');
+          // err.type.should.equal('custom');
+          // err.statusCode.should.equal(501);
 
           // translate the HTTPError to a normal response
           ctx.body = err.body;
@@ -723,7 +602,7 @@ describe('test/lib/router.test.js', function() {
       app.use(router.allowedMethods({
         throw: true,
         notImplemented() {
-          const notImplementedErr = new Error('Custom Not Implemented Error');
+          const notImplementedErr: any = new Error('Custom Not Implemented Error');
           notImplementedErr.type = 'custom';
           notImplementedErr.statusCode = 501;
           notImplementedErr.body = {
@@ -736,22 +615,19 @@ describe('test/lib/router.test.js', function() {
       }));
       router.get('/users', function() {});
       router.put('/users', function() {});
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .search('/users')
-        .expect(501)
-        .end(function(err, res) {
-          if (err) return done(err);
-          // the 'Allow' header is not set when throwing
-          res.header.should.not.have.property('allow');
-          res.body.should.eql({ error: 'Custom Not Implemented Error',
-            statusCode: 501,
-            otherStuff: true,
-          });
-          done();
-        });
+        .expect(501);
+      // the 'Allow' header is not set when throwing
+      assert.equal(res.header.allow, undefined);
+      assert.deepEqual(res.body, {
+        error: 'Custom Not Implemented Error',
+        statusCode: 501,
+        otherStuff: true,
+      });
     });
 
-    it('does not send 405 if route matched but status is 404', function(done) {
+    it('does not send 405 if route matched but status is 404', async () => {
       const app = new Koa();
       const router = new Router();
       app.use(router.routes());
@@ -759,16 +635,12 @@ describe('test/lib/router.test.js', function() {
       router.get('/users', function(ctx) {
         ctx.status = 404;
       });
-      request(http.createServer(app.callback()))
+      await request(app.callback())
         .get('/users')
-        .expect(404)
-        .end(function(err) {
-          if (err) return done(err);
-          done();
-        });
+        .expect(404);
     });
 
-    it('sets the allowed methods to a single Allow header #273', function(done) {
+    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();
@@ -777,21 +649,14 @@ describe('test/lib/router.test.js', function() {
 
       router.get('/', function() {});
 
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .options('/')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          res.header.should.have.property('allow', 'HEAD, GET');
-          const allowHeaders = res.res.rawHeaders.filter(item => item === 'Allow');
-          expect(allowHeaders.length).to.eql(1);
-          done();
-        });
+        .expect(200);
+      assert.equal(res.header.allow, 'HEAD, GET');
     });
-
   });
 
-  it('supports custom routing detect path: ctx.routerPath', function(done) {
+  it('supports custom routing detect path: ctx.routerPath', async () => {
     const app = new Koa();
     const router = new Router();
     app.use(function(ctx, next) {
@@ -805,65 +670,66 @@ describe('test/lib/router.test.js', function() {
       ctx.body = ctx.method + ' ' + ctx.url;
     });
 
-    request(http.createServer(app.callback()))
+    await request(app.callback())
       .get('/users')
       .set('Host', 'helloworld.example.com')
       .expect(200)
-      .expect('GET /users', done);
+      .expect('GET /users');
   });
 
-  describe('Router#[verb]()', function() {
-    it('registers route specific to HTTP verb', function() {
+  describe('Router#[verb]()', () => {
+    it('registers route specific to HTTP verb', () => {
       const app = new Koa();
       const router = new Router();
       app.use(router.routes());
       methods.forEach(function(method) {
-        router.should.have.property(method);
-        router[method].should.be.type('function');
-        router[method]('/', function() {});
+        assert(method in router);
+        assert(typeof Reflect.get(router, method) === 'function');
+        Reflect.get(router, method).call(router, '/', function() {});
       });
-      router.stack.should.have.length(methods.length);
+      assert.equal(router.stack.length, methods.length);
     });
 
-    it('registers route with a regexp path', function() {
+    it('registers route with a regexp path', () => {
       const router = new Router();
       methods.forEach(function(method) {
-        router[method](/^\/\w$/i, function() {}).should.equal(router);
+        assert.equal(Reflect.get(router, method).call(router, /^\/\w$/i, function() {}), router);
       });
     });
 
-    it('registers route with a given name', function() {
+    it('registers route with a given name', () => {
       const router = new Router();
       methods.forEach(function(method) {
-        router[method](method, '/', function() {}).should.equal(router);
+        assert.equal(Reflect.get(router, method).call(router, '/', function() {}), router);
       });
     });
 
-    it('registers route with with a given name and regexp path', function() {
+    it('registers route with with a given name and regexp path', () => {
       const router = new Router();
       methods.forEach(function(method) {
-        router[method](method, /^\/$/i, function() {}).should.equal(router);
+        assert.equal(Reflect.get(router, method).call(router, /^\/$/i, function() {}), router);
       });
     });
 
-    it('enables route chaining', function() {
+    it('enables route chaining', () => {
       const router = new Router();
       methods.forEach(function(method) {
-        router[method]('/', function() {}).should.equal(router);
+        assert(Reflect.get(router, method.toLowerCase()), `${method.toLowerCase()} not exists`);
+        assert.equal(Reflect.get(router, method.toLowerCase()).call(router, '/', function() {}), router);
       });
     });
 
-    it('registers array of paths (gh-203)', function() {
+    it('registers array of paths (gh-203)', () => {
       const router = new Router();
-      router.get([ '/one', '/two' ], function(ctx, next) {
+      router.get([ '/one', '/two' ], function(_ctx, next) {
         return next();
       });
-      expect(router.stack).to.have.property('length', 2);
-      expect(router.stack[0]).to.have.property('path', '/one');
-      expect(router.stack[1]).to.have.property('path', '/two');
+      assert.equal(router.stack.length, 2);
+      assert.equal(router.stack[0].path, '/one');
+      assert.equal(router.stack[1].path, '/two');
     });
 
-    it('resolves non-parameterized routes without attached parameters', function(done) {
+    it('resolves non-parameterized routes without attached parameters', async () => {
       const app = new Koa();
       const router = new Router();
 
@@ -884,23 +750,17 @@ describe('test/lib/router.test.js', function() {
       });
 
       app.use(router.routes());
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .get('/notparameter')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-
-          expect(res.body.param).to.equal(undefined);
-          expect(res.body.routerName).to.equal(null);
-          expect(res.body.routerPath).to.equal('/notparameter');
-          done();
-        });
+        .expect(200);
+      assert.equal(res.body.param, undefined);
+      assert.equal(res.body.routerName, undefined);
+      assert.equal(res.body.routerPath, '/notparameter');
     });
-
   });
 
-  describe('Router#use()', function() {
-    it('uses router middleware without path', function(done) {
+  describe('Router#use()', () => {
+    it('uses router middleware without path', async () => {
       const app = new Koa();
       const router = new Router();
 
@@ -921,18 +781,13 @@ describe('test/lib/router.test.js', function() {
       });
 
       app.use(router.routes());
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .get('/foo/bar')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-
-          expect(res.body).to.have.property('foobar', 'foobar');
-          done();
-        });
+        .expect(200);
+      assert.equal(res.body.foobar, 'foobar');
     });
 
-    it('uses router middleware at given path', function(done) {
+    it('uses router middleware at given path', async () => {
       const app = new Koa();
       const router = new Router();
 
@@ -948,18 +803,13 @@ describe('test/lib/router.test.js', function() {
       });
 
       app.use(router.routes());
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .get('/foo/bar')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-
-          expect(res.body).to.have.property('foobar', 'foobar');
-          done();
-        });
+        .expect(200);
+      assert.equal(res.body.foobar, 'foobar');
     });
 
-    it('runs router middleware before subrouter middleware', function(done) {
+    it('runs router middleware before subrouter middleware', async () => {
       const app = new Koa();
       const router = new Router();
       const subrouter = new Router();
@@ -982,18 +832,13 @@ describe('test/lib/router.test.js', function() {
 
       router.use('/foo', subrouter.routes());
       app.use(router.routes());
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .get('/foo/bar')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-
-          expect(res.body).to.have.property('foobar', 'foobar');
-          done();
-        });
+        .expect(200);
+      assert.equal(res.body.foobar, 'foobar');
     });
 
-    it('assigns middleware to array of paths', function(done) {
+    it('assigns middleware to array of paths', async () => {
       const app = new Koa();
       const router = new Router();
 
@@ -1016,28 +861,21 @@ describe('test/lib/router.test.js', function() {
       });
 
       app.use(router.routes());
-      request(http.createServer(app.callback()))
+      let res = await request(app.callback())
         .get('/foo')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          expect(res.body).to.have.property('foobar', 'foobar');
-          request(http.createServer(app.callback()))
-            .get('/bar')
-            .expect(200)
-            .end(function(err, res) {
-              if (err) return done(err);
-              expect(res.body).to.have.property('foobar', 'foobar');
-              done();
-            });
-        });
+        .expect(200);
+      assert.equal(res.body.foobar, 'foobar');
+      res = await request(app.callback())
+        .get('/bar')
+        .expect(200);
+      assert.equal(res.body.foobar, 'foobar');
     });
 
-    it('without path, does not set params.0 to the matched path - gh-247', function(done) {
+    it('without path, does not set params.0 to the matched path - gh-247', async () => {
       const app = new Koa();
       const router = new Router();
 
-      router.use(function(ctx, next) {
+      router.use(function(_ctx, next) {
         return next();
       });
 
@@ -1046,19 +884,14 @@ describe('test/lib/router.test.js', function() {
       });
 
       app.use(router.routes());
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .get('/foo/815')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-
-          expect(res.body).to.have.property('id', '815');
-          expect(res.body).to.not.have.property('0');
-          done();
-        });
+        .expect(200);
+      assert.equal(res.body.id, '815');
+      assert.equal(res.body['0'], undefined);
     });
 
-    it('does not add an erroneous (.*) to unprefiexed nested routers - gh-369 gh-410', function(done) {
+    it('does not add an erroneous (.*) to unprefiexed nested routers - gh-369 gh-410', async () => {
       const app = new Koa();
       const router = new Router();
       const nested = new Router();
@@ -1079,78 +912,66 @@ describe('test/lib/router.test.js', function() {
       router.use(nested.routes());
       app.use(router.routes());
 
-      request(app.callback())
+      await request(app.callback())
         .get('/test')
         .expect(200)
-        .expect('test')
-        .end(function(err) {
-          if (err) return done(err);
-          expect(called).to.eql(1, 'too many routes matched');
-          done();
-        });
+        .expect('test');
+      assert.equal(called, 1);
     });
   });
 
-  describe('Router#register()', function() {
-    it('registers new routes', function(done) {
+  describe('Router#register()', () => {
+    it('registers new routes', () => {
       const app = new Koa();
       const router = new Router();
-      router.should.have.property('register');
-      router.register.should.be.type('function');
+      assert(typeof router.register === 'function');
       const route = router.register('/', [ 'GET', 'POST' ], function() {});
-      should.exists(route);
+      assert(route);
       app.use(router.routes());
-      router.stack.should.be.an.instanceOf(Array);
-      router.stack.should.have.property('length', 1);
-      router.stack[0].should.have.property('path', '/');
-      done();
+      assert.equal(router.stack.length, 1);
+      assert.equal(router.stack[0].path, '/');
     });
   });
 
-  describe('Router#redirect()', function() {
-    it('registers redirect routes', function(done) {
+  describe('Router#redirect()', () => {
+    it('registers redirect routes', () => {
       const app = new Koa();
       const router = new Router();
-      router.should.have.property('redirect');
-      router.redirect.should.be.type('function');
+      assert(typeof router.redirect === 'function');
       router.redirect('/source', '/destination', 302);
       app.use(router.routes());
-      router.stack.should.have.property('length', 1);
-      router.stack[0].should.be.instanceOf(Layer);
-      router.stack[0].should.have.property('path', '/source');
-      done();
+      assert.equal(router.stack.length, 1);
+      assert.equal(router.stack[0].path, '/source');
     });
 
-    it('redirects using route names', function(done) {
+    it('redirects using route names', async () => {
       const app = new Koa();
       const router = new Router();
       app.use(router.routes());
       router.get('home', '/', function() {});
       router.get('sign-up-form', '/sign-up-form', function() {});
       router.redirect('home', 'sign-up-form');
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .post('/')
-        .expect(301)
-        .end(function(err, res) {
-          if (err) return done(err);
-          res.header.should.have.property('location', '/sign-up-form');
-          done();
-        });
+        .expect(301);
+      assert.equal(res.headers.location, '/sign-up-form');
     });
   });
 
-  describe('Router#route()', function() {
-    it('inherits routes from nested router', function() {
+  describe('Router#route()', () => {
+    it('inherits routes from nested router', () => {
       const subrouter = new Router().get('child', '/hello', function(ctx) {
         ctx.body = { hello: 'world' };
       });
       const router = new Router().use(subrouter.routes());
-      expect(router.route('child')).to.have.property('name', 'child');
+      const route = router.route('child');
+      assert(route);
+      assert.equal(route.name, 'child');
     });
   });
 
-  describe('Router#url()', function() {
-    it('generates URL for given route name', function(done) {
+  describe('Router#url()', () => {
+    it('generates URL for given route name', () => {
       const app = new Koa();
       const router = new Router();
       app.use(router.routes());
@@ -1158,16 +979,16 @@ describe('test/lib/router.test.js', function() {
         ctx.status = 204;
       });
       let url = router.url('books', { category: 'programming', title: 'how to node' });
-      url.should.equal('/programming/how%20to%20node');
+      assert.equal(url, '/programming/how%20to%20node');
       url = router.url('books', 'programming', 'how to node');
-      url.should.equal('/programming/how%20to%20node');
+      assert.equal(url, '/programming/how%20to%20node');
 
       const err = router.url('not-exists', { category: 'programming', title: 'how to node' });
-      err.message.should.equal('No route found for name: not-exists');
-      done();
+      assert(err instanceof Error);
+      assert.equal(err.message, 'No route found for name: not-exists');
     });
 
-    it('generates URL for given route name within embedded routers', function(done) {
+    it('generates URL for given route name within embedded routers', () => {
       const app = new Koa();
       const router = new Router({
         prefix: '/books',
@@ -1182,13 +1003,12 @@ describe('test/lib/router.test.js', function() {
       router.use(embeddedRouter.routes());
       app.use(router.routes());
       let url = router.url('chapters', { chapterName: 'Learning ECMA6', pageNumber: 123 });
-      url.should.equal('/books/chapters/Learning%20ECMA6/123');
+      assert.equal(url, '/books/chapters/Learning%20ECMA6/123');
       url = router.url('chapters', 'Learning ECMA6', 123);
-      url.should.equal('/books/chapters/Learning%20ECMA6/123');
-      done();
+      assert.equal(url, '/books/chapters/Learning%20ECMA6/123');
     });
 
-    it('generates URL for given route name within two embedded routers', function(done) {
+    it('generates URL for given route name within two embedded routers', () => {
       const app = new Koa();
       const router = new Router({
         prefix: '/books',
@@ -1206,11 +1026,10 @@ describe('test/lib/router.test.js', function() {
       router.use(embeddedRouter.routes());
       app.use(router.routes());
       const url = router.url('chapters', { chapterName: 'Learning ECMA6', pageNumber: 123 });
-      url.should.equal('/books/chapters/Learning%20ECMA6/pages/123');
-      done();
+      assert.equal(url, '/books/chapters/Learning%20ECMA6/pages/123');
     });
 
-    it('generates URL for given route name with params and query params', function(done) {
+    it('generates URL for given route name with params and query params', () => {
       const router = new Router();
       router.get('books', '/books/:category/:id', function(ctx) {
         ctx.status = 204;
@@ -1218,21 +1037,20 @@ describe('test/lib/router.test.js', function() {
       let url = router.url('books', 'programming', 4, {
         query: { page: 3, limit: 10 },
       });
-      url.should.equal('/books/programming/4?page=3&limit=10');
+      assert.equal(url, '/books/programming/4?page=3&limit=10');
       url = router.url('books',
         { category: 'programming', id: 4 },
-        { query: { page: 3, limit: 10 } }
+        { query: { page: 3, limit: 10 } },
       );
-      url.should.equal('/books/programming/4?page=3&limit=10');
+      assert.equal(url, '/books/programming/4?page=3&limit=10');
       url = router.url('books',
         { category: 'programming', id: 4 },
-        { query: 'page=3&limit=10' }
+        { query: 'page=3&limit=10' },
       );
-      url.should.equal('/books/programming/4?page=3&limit=10');
-      done();
+      assert.equal(url, '/books/programming/4?page=3&limit=10');
     });
 
-    it('generates URL for given route name without params and query params', function(done) {
+    it('generates URL for given route name without params and query params', () => {
       const router = new Router();
       router.get('category', '/category', function(ctx) {
         ctx.status = 204;
@@ -1240,13 +1058,12 @@ describe('test/lib/router.test.js', function() {
       const url = router.url('category', {
         query: { page: 3, limit: 10 },
       });
-      url.should.equal('/category?page=3&limit=10');
-      done();
+      assert.equal(url, '/category?page=3&limit=10');
     });
   });
 
-  describe('Router#param()', function() {
-    it('runs parameter middleware', function(done) {
+  describe('Router#param()', () => {
+    it('runs parameter middleware', async () => {
       const app = new Koa();
       const router = new Router();
       app.use(router.routes());
@@ -1262,18 +1079,13 @@ describe('test/lib/router.test.js', function() {
         .get('/users/:user', function(ctx) {
           ctx.body = ctx.user;
         });
-      request(http.createServer(app.callback()))
+      const res = await request(app.callback())
         .get('/users/3')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          res.should.have.property('body');
-          res.body.should.have.property('name', 'alex');
-          done();
-        });
+        .expect(200);
+      assert.equal(res.body.name, 'alex');
     });
 
-    it('runs parameter middleware in order of URL appearance', function(done) {
+    it('runs parameter middleware in order of URL appearance', async () => {
       const app = new Koa();
       const router = new Router();
       router
@@ -1303,22 +1115,14 @@ describe('test/lib/router.test.js', function() {
           ctx.body = ctx.user;
         });
 
-      request(http.createServer(
-        app
-          .use(router.routes())
-          .callback()))
+      const res = await request(app.use(router.routes()).callback())
         .get('/first/users/3')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          res.should.have.property('body');
-          res.body.should.have.property('name', 'alex');
-          res.body.should.have.property('ordered', 'parameters');
-          done();
-        });
+        .expect(200);
+      assert.equal(res.body.name, 'alex');
+      assert.equal(res.body.ordered, 'parameters');
     });
 
-    it('runs parameter middleware in order of URL appearance even when added in random order', function(done) {
+    it('runs parameter middleware in order of URL appearance even when added in random order', async () => {
       const app = new Koa();
       const router = new Router();
       router
@@ -1343,21 +1147,13 @@ describe('test/lib/router.test.js', function() {
           ctx.body = ctx.state.loaded;
         });
 
-      request(http.createServer(
-        app
-          .use(router.routes())
-          .callback()))
+      const res = await request(app.use(router.routes()).callback())
         .get('/1/2/3/4')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          res.should.have.property('body');
-          res.body.should.eql([ '1', '2', '3', '4' ]);
-          done();
-        });
+        .expect(200);
+      assert.deepEqual(res.body, [ '1', '2', '3', '4' ]);
     });
 
-    it('runs parent parameter middleware for subrouter', function(done) {
+    it('runs parent parameter middleware for subrouter', async () => {
       const app = new Koa();
       const router = new Router();
       const subrouter = new Router();
@@ -1378,21 +1174,16 @@ describe('test/lib/router.test.js', function() {
         })
         .use('/:id/children', subrouter.routes());
 
-      request(http.createServer(app.use(router.routes()).callback()))
+      const res = await request(app.use(router.routes()).callback())
         .get('/did-not-run/children/2')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          res.should.have.property('body');
-          res.body.should.have.property('id', 'ran');
-          res.body.should.have.property('cid', '2');
-          done();
-        });
+        .expect(200);
+      assert.deepEqual(res.body.id, 'ran');
+      assert.deepEqual(res.body.cid, '2');
     });
   });
 
-  describe('Router#opts', function() {
-    it('responds with 200', function(done) {
+  describe('Router#opts', () => {
+    it('responds with 200', async () => {
       const app = new Koa();
       const router = new Router({
         strict: true,
@@ -1400,20 +1191,13 @@ describe('test/lib/router.test.js', function() {
       router.get('/info', function(ctx) {
         ctx.body = 'hello';
       });
-      request(http.createServer(
-        app
-          .use(router.routes())
-          .callback()))
+      const res = await request(app.use(router.routes()).callback())
         .get('/info')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          res.text.should.equal('hello');
-          done();
-        });
+        .expect(200);
+      assert.equal(res.text, 'hello');
     });
 
-    it('should allow setting a prefix', function(done) {
+    it('should allow setting a prefix', async () => {
       const app = new Koa();
       const routes = new Router({ prefix: '/things/:thing_id' });
 
@@ -1421,19 +1205,13 @@ describe('test/lib/router.test.js', function() {
         ctx.body = ctx.params;
       });
 
-      app.use(routes.routes());
-
-      request(http.createServer(app.callback()))
+      const res = await request(app.use(routes.routes()).callback())
         .get('/things/1/list')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          res.body.thing_id.should.equal('1');
-          done();
-        });
+        .expect(200);
+      assert.equal(res.body.thing_id, '1');
     });
 
-    it('responds with 404 when has a trailing slash', function(done) {
+    it('responds with 404 when has a trailing slash', async () => {
       const app = new Koa();
       const router = new Router({
         strict: true,
@@ -1441,21 +1219,14 @@ describe('test/lib/router.test.js', function() {
       router.get('/info', function(ctx) {
         ctx.body = 'hello';
       });
-      request(http.createServer(
-        app
-          .use(router.routes())
-          .callback()))
+      await request(app.use(router.routes()).callback())
         .get('/info/')
-        .expect(404)
-        .end(function(err) {
-          if (err) return done(err);
-          done();
-        });
+        .expect(404);
     });
   });
 
-  describe('use middleware with opts', function() {
-    it('responds with 200', function(done) {
+  describe('use middleware with opts', () => {
+    it('responds with 200', async () => {
       const app = new Koa();
       const router = new Router({
         strict: true,
@@ -1463,20 +1234,13 @@ describe('test/lib/router.test.js', function() {
       router.get('/info', function(ctx) {
         ctx.body = 'hello';
       });
-      request(http.createServer(
-        app
-          .use(router.routes())
-          .callback()))
+      const res = await request(app.use(router.routes()).callback())
         .get('/info')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          res.text.should.equal('hello');
-          done();
-        });
+        .expect(200);
+      assert.equal(res.text, 'hello');
     });
 
-    it('responds with 404 when has a trailing slash', function(done) {
+    it('responds with 404 when has a trailing slash', async () => {
       const app = new Koa();
       const router = new Router({
         strict: true,
@@ -1484,219 +1248,163 @@ describe('test/lib/router.test.js', function() {
       router.get('/info', function(ctx) {
         ctx.body = 'hello';
       });
-      request(http.createServer(
-        app
-          .use(router.routes())
-          .callback()))
+      await request(app.use(router.routes()).callback())
         .get('/info/')
-        .expect(404)
-        .end(function(err) {
-          if (err) return done(err);
-          done();
-        });
+        .expect(404);
     });
   });
 
-  describe('router.routes()', function() {
-    it('should return composed middleware', function(done) {
+  describe('router.routes()', () => {
+    it('should return composed middleware', async () => {
       const app = new Koa();
       const router = new Router();
       let middlewareCount = 0;
-      const middlewareA = function(ctx, next) {
+      const middlewareA = function(_ctx: any, next: Next) {
         middlewareCount++;
         return next();
       };
-      const middlewareB = function(ctx, next) {
+      const middlewareB = function(_ctx: any, next: Next) {
         middlewareCount++;
         return next();
       };
 
       router.use(middlewareA, middlewareB);
       router.get('/users/:id', function(ctx) {
-        should.exist(ctx.params.id);
+        assert(ctx.params.id);
         ctx.body = { hello: 'world' };
       });
 
       const routerMiddleware = router.routes();
+      assert(typeof routerMiddleware === 'function');
 
-      expect(routerMiddleware).to.be.a('function');
-
-      request(http.createServer(
-        app
-          .use(routerMiddleware)
-          .callback()))
+      const res = await request(app.use(routerMiddleware).callback())
         .get('/users/1')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          expect(res.body).to.be.an('object');
-          expect(res.body).to.have.property('hello', 'world');
-          expect(middlewareCount).to.equal(2);
-          done();
-        });
+        .expect(200);
+      assert.equal(res.body.hello, 'world');
+      assert.equal(middlewareCount, 2);
     });
 
-    it('places a `_matchedRoute` value on context', function(done) {
+    it('places a `_matchedRoute` value on context', async () => {
       const app = new Koa();
       const router = new Router();
-      const middleware = function(ctx, next) {
-        expect(ctx._matchedRoute).to.be('/users/:id');
+      const middleware = function(ctx: any, next: Next) {
+        assert.equal(ctx._matchedRoute, '/users/:id');
         return next();
       };
 
       router.get('/users/:id', middleware, function(ctx) {
-        expect(ctx._matchedRoute).to.be('/users/:id');
-        should.exist(ctx.params.id);
+        assert.equal(ctx._matchedRoute, '/users/:id');
+        assert(ctx.params.id);
         ctx.body = { hello: 'world' };
       });
 
       const routerMiddleware = router.routes();
 
-      request(http.createServer(
-        app
-          .use(routerMiddleware)
-          .callback()))
+      await request(app.use(routerMiddleware).callback())
         .get('/users/1')
-        .expect(200)
-        .end(function(err) {
-          if (err) return done(err);
-          done();
-        });
+        .expect(200);
     });
 
-    it('places a `_matchedRouteName` value on the context for a named route', function(done) {
+    it('places a `_matchedRouteName` value on the context for a named route', async () => {
       const app = new Koa();
       const router = new Router();
 
       router.get('users#show', '/users/:id', function(ctx) {
-        expect(ctx._matchedRouteName).to.be('users#show');
+        assert.equal(ctx._matchedRouteName, 'users#show');
         ctx.status = 200;
       });
 
-      request(http.createServer(app.use(router.routes()).callback()))
+      await request(app.use(router.routes()).callback())
         .get('/users/1')
-        .expect(200)
-        .end(function(err) {
-          if (err) return done(err);
-          done();
-        });
+        .expect(200);
     });
 
-    it('does not place a `_matchedRouteName` value on the context for unnamed routes', function(done) {
+    it('does not place a `_matchedRouteName` value on the context for unnamed routes', async () => {
       const app = new Koa();
       const router = new Router();
 
       router.get('/users/:id', function(ctx) {
-        expect(ctx._matchedRouteName).to.be(undefined);
+        assert.equal(ctx._matchedRouteName, undefined);
         ctx.status = 200;
       });
 
-      request(http.createServer(app.use(router.routes()).callback()))
+      await request(app.use(router.routes()).callback())
         .get('/users/1')
-        .expect(200)
-        .end(function(err) {
-          if (err) return done(err);
-          done();
-        });
+        .expect(200);
     });
 
-    it('routerName and routerPath work with next', function(done) {
+    it('routerName and routerPath work with next', async () => {
       const app = new Koa();
       const router = new Router();
       router.get('name1', '/users/1', function(ctx, next) {
-        expect(ctx._matchedRouteName).to.be('name1');
-        expect(ctx.routerName).to.be('name1');
-        expect(ctx._matchedRoute).to.be('/users/1');
-        expect(ctx.routerPath).to.be('/users/1');
+        assert.equal(ctx._matchedRouteName, 'name1');
+        assert.equal(ctx.routerName, 'name1');
+        assert.equal(ctx._matchedRoute, '/users/1');
+        assert.equal(ctx.routerPath, '/users/1');
         return next();
       });
       router.get('name2', '/users/:id', function(ctx) {
-        expect(ctx._matchedRouteName).to.be('name2');
-        expect(ctx.routerName).to.be('name2');
-        expect(ctx._matchedRoute).to.be('/users/:id');
-        expect(ctx.routerPath).to.be('/users/:id');
+        assert.equal(ctx._matchedRouteName, 'name2');
+        assert.equal(ctx.routerName, 'name2');
+        assert.equal(ctx._matchedRoute, '/users/:id');
+        assert.equal(ctx.routerPath, '/users/:id');
         ctx.status = 200;
       });
 
-      request(http.createServer(app.use(router.routes()).callback()))
+      await request(app.use(router.routes()).callback())
         .get('/users/1')
-        .expect(200)
-        .end(function(err) {
-          if (err) return done(err);
-          done();
-        });
+        .expect(200);
     });
   });
 
-  describe('If no HEAD method, default to GET', function() {
-    it('should default to GET', function(done) {
-      const app = new Koa();
-      const router = new Router();
-      router.get('/users/:id', function(ctx) {
-        should.exist(ctx.params.id);
-        ctx.body = 'hello';
-      });
-      request(http.createServer(
-        app
-          .use(router.routes())
-          .callback()))
-        .head('/users/1')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          expect(res.body).to.be.empty();
-          done();
-        });
-    });
-
-    it('should work with middleware', function(done) {
+  describe('If no HEAD method, default to GET', () => {
+    it('should default to GET', async () => {
       const app = new Koa();
       const router = new Router();
+      console.log(router);
       router.get('/users/:id', function(ctx) {
-        should.exist(ctx.params.id);
+        assert(ctx.params.id);
         ctx.body = 'hello';
       });
-      request(http.createServer(
-        app
-          .use(router.routes())
-          .callback()))
+      app.use(router.routes());
+      let res = await request(app.callback())
+        .get('/users/1')
+        .expect(200);
+      assert.equal(res.text, 'hello');
+      res = await request(app.callback())
         .head('/users/1')
-        .expect(200)
-        .end(function(err, res) {
-          if (err) return done(err);
-          expect(res.body).to.be.empty();
-          done();
-        });
+        .expect(200);
+      assert.equal(res.text, '');
     });
   });
 
-  describe('Router#prefix', function() {
-    it('should set opts.prefix', function() {
+  describe('Router#prefix', () => {
+    it('should set opts.prefix', () => {
       const router = new Router();
-      expect(router.opts).to.not.have.key('prefix');
+      assert.equal(router.opts.prefix, undefined);
       router.prefix('/things/:thing_id');
-      expect(router.opts.prefix).to.equal('/things/:thing_id');
+      assert.equal(router.opts.prefix, '/things/:thing_id');
     });
 
-    it('should prefix existing routes', function() {
+    it('should prefix existing routes', () => {
       const router = new Router();
       router.get('/users/:id', function(ctx) {
         ctx.body = 'test';
       });
       router.prefix('/things/:thing_id');
       const route = router.stack[0];
-      expect(route.path).to.equal('/things/:thing_id/users/:id');
-      expect(route.paramNames).to.have.length(2);
-      expect(route.paramNames[0]).to.have.property('name', 'thing_id');
-      expect(route.paramNames[1]).to.have.property('name', 'id');
+      assert.equal(route.path, '/things/:thing_id/users/:id');
+      assert.equal(route.paramNames.length, 2);
+      assert.equal(route.paramNames[0].name, 'thing_id');
+      assert.equal(route.paramNames[1].name, 'id');
     });
 
-    describe('when used with .use(fn) - gh-247', function() {
-      it('does not set params.0 to the matched path', function(done) {
+    describe('when used with .use(fn) - gh-247', () => {
+      it('does not set params.0 to the matched path', async () => {
         const app = new Koa();
         const router = new Router();
 
-        router.use(function(ctx, next) {
+        router.use(function(_ctx, next) {
           return next();
         });
 
@@ -1707,25 +1415,20 @@ describe('test/lib/router.test.js', function() {
         router.prefix('/things');
 
         app.use(router.routes());
-        request(http.createServer(app.callback()))
+        const res = await request(app.callback())
           .get('/things/foo/108')
-          .expect(200)
-          .end(function(err, res) {
-            if (err) return done(err);
-
-            expect(res.body).to.have.property('id', '108');
-            expect(res.body).to.not.have.property('0');
-            done();
-          });
+          .expect(200);
+        assert.equal(res.body.id, '108');
+        assert.equal(res.body['0'], undefined);
       });
     });
 
     describe('with trailing slash', testPrefix('/admin/'));
     describe('without trailing slash', testPrefix('/admin'));
 
-    function testPrefix(prefix) {
-      return function() {
-        let server;
+    function testPrefix(prefix: string) {
+      return () => {
+        let server: any;
         let middlewareCount = 0;
 
         before(function() {
@@ -1744,94 +1447,73 @@ describe('test/lib/router.test.js', function() {
           });
 
           router.prefix(prefix);
-          server = http.createServer(app.use(router.routes()).callback());
-        });
-
-        after(function() {
-          server.close();
+          server = app.use(router.routes()).callback();
         });
 
-        beforeEach(function() {
+        beforeEach(() => {
           middlewareCount = 0;
         });
 
-        it('should support root level router middleware', function(done) {
-          request(server)
+        it('should support root level router middleware', async () => {
+          const res = await request(server)
             .get(prefix)
-            .expect(200)
-            .end(function(err, res) {
-              if (err) return done(err);
-              expect(middlewareCount).to.equal(2);
-              expect(res.body).to.be.an('object');
-              expect(res.body).to.have.property('name', 'worked');
-              done();
-            });
+            .expect(200);
+          assert.equal(middlewareCount, 2);
+          assert.equal(res.body.name, 'worked');
         });
 
-        it('should support requests with a trailing path slash', function(done) {
-          request(server)
+        it('should support requests with a trailing path slash', async () => {
+          const res = await request(server)
             .get('/admin/')
-            .expect(200)
-            .end(function(err, res) {
-              if (err) return done(err);
-              expect(middlewareCount).to.equal(2);
-              expect(res.body).to.be.an('object');
-              expect(res.body).to.have.property('name', 'worked');
-              done();
-            });
+            .expect(200);
+          assert.equal(middlewareCount, 2);
+          assert.equal(res.body.name, 'worked');
         });
 
-        it('should support requests without a trailing path slash', function(done) {
-          request(server)
+        it('should support requests without a trailing path slash', async () => {
+          const res = await request(server)
             .get('/admin')
-            .expect(200)
-            .end(function(err, res) {
-              if (err) return done(err);
-              expect(middlewareCount).to.equal(2);
-              expect(res.body).to.be.an('object');
-              expect(res.body).to.have.property('name', 'worked');
-              done();
-            });
+            .expect(200);
+          assert.equal(middlewareCount, 2);
+          assert.equal(res.body.name, 'worked');
         });
       };
     }
   });
 
-  describe('Static Router#url()', function() {
-    it('generates route URL', function() {
+  describe('Static Router#url()', () => {
+    it('generates route URL', () => {
       const url = Router.url('/:category/:title', { category: 'programming', title: 'how-to-node' });
-      url.should.equal('/programming/how-to-node');
+      assert.equal(url, '/programming/how-to-node');
     });
 
-    it('escapes using encodeURIComponent()', function() {
+    it('escapes using encodeURIComponent()', () => {
       const url = Router.url('/:category/:title', { category: 'programming', title: 'how to node' });
-      url.should.equal('/programming/how%20to%20node');
+      assert.equal(url, '/programming/how%20to%20node');
     });
 
-    it('generates route URL with params and query params', function(done) {
+    it('generates route URL with params and query params', () => {
       let url = Router.url('/books/:category/:id', 'programming', 4, {
         query: { page: 3, limit: 10 },
       });
-      url.should.equal('/books/programming/4?page=3&limit=10');
+      assert.equal(url, '/books/programming/4?page=3&limit=10');
       url = Router.url('/books/:category/:id',
         { category: 'programming', id: 4 },
-        { query: { page: 3, limit: 10 } }
+        { query: { page: 3, limit: 10 } },
       );
-      url.should.equal('/books/programming/4?page=3&limit=10');
+      assert.equal(url, '/books/programming/4?page=3&limit=10');
       url = Router.url('/books/:category/:id',
         { category: 'programming', id: 4 },
-        { query: 'page=3&limit=10' }
+        { query: 'page=3&limit=10' },
       );
-      url.should.equal('/books/programming/4?page=3&limit=10');
-      done();
+      assert.equal(url, '/books/programming/4?page=3&limit=10');
     });
 
-    it('generates router URL without params and with with query params', function(done) {
+    it('generates router URL without params and with with query params', () => {
       const url = Router.url('/category', {
         query: { page: 3, limit: 10 },
       });
-      url.should.equal('/category?page=3&limit=10');
-      done();
+      assert.equal(url, '/category?page=3&limit=10');
     });
   });
 });
diff --git a/test/index.test.ts b/test/index.test.ts
index 837e500..71c2b15 100644
--- a/test/index.test.ts
+++ b/test/index.test.ts
@@ -1,10 +1,11 @@
 import { strict as assert } from 'node:assert';
-import Router, { KoaRouter } from '../src/index.js';
+import Router, { KoaRouter, EggRouter } from '../src/index.js';
 
 describe('test/index.test.ts', () => {
   it('should expose Router', () => {
     assert(typeof Router === 'function');
     assert(typeof KoaRouter === 'function');
-    // assert(typeof Router.EggRouter === 'function');
+    assert.equal(Router, KoaRouter);
+    assert(typeof EggRouter === 'function');
   });
 });
diff --git a/test/utils.test.ts b/test/utils.test.ts
deleted file mode 100644
index a6c6f4c..0000000
--- a/test/utils.test.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-'use strict';
-
-const utils = require('../../lib/utils');
-const is = require('is-type-of');
-const assert = require('assert');
-
-describe('test/lib/utils.test.js', () => {
-  describe('callFn', () => {
-    it('should not function return same', () => {
-      const res = utils.callFn('foo');
-      assert(is.promise(res));
-      return res.then(result => assert(result === undefined));
-    });
-
-    it('should async function return promise', () => {
-      const res = utils.callFn(async (foo, bar) => {
-        return foo + bar;
-      }, [ 1, 2 ]);
-      assert(is.promise(res));
-      return res.then(result => assert(result === 3));
-    });
-
-    it('should generator function return promise', () => {
-      const res = utils.callFn(function* (foo, bar) {
-        return foo + bar;
-      }, [ 1, 2 ]);
-      assert(is.promise(res));
-      return res.then(result => assert(result === 3));
-    });
-
-    it('should common function return promise', () => {
-      const res = utils.callFn((foo, bar) => {
-        return foo + bar;
-      }, [ 1, 2 ]);
-      assert(is.promise(res));
-      return res.then(result => assert(result === 3));
-    });
-
-    it('should work with ctx', () => {
-      const res = utils.callFn(async function(bar) {
-        return this.foo + bar;
-      }, [ 2 ], { foo: 1 });
-      assert(is.promise(res));
-      return res.then(result => assert(result === 3));
-    });
-  });
-
-  describe('middleware', () => {
-    it('should work with async function', () => {
-      const res = utils.middleware(async () => {});
-      assert(is.asyncFunction(res));
-    });
-
-    it('should work with generator function', () => {
-      const res = utils.middleware(function* () {
-        return;
-      });
-      assert(!is.generatorFunction(res));
-    });
-  });
-});

From 711543277d28c0ad3b5746dd5026a383de757366 Mon Sep 17 00:00:00 2001
From: fengmk2 <suqian.yf@antgroup.com>
Date: Tue, 11 Jun 2024 14:35:20 +0800
Subject: [PATCH 10/13] f

---
 src/EggRouter.ts       | 87 ++++++++++++++++++------------------------
 src/Router.ts          | 26 ++++++-------
 test/EggRouter.test.ts | 25 ++++++++++++
 3 files changed, 74 insertions(+), 64 deletions(-)

diff --git a/src/EggRouter.ts b/src/EggRouter.ts
index f5e38b6..965bce0 100644
--- a/src/EggRouter.ts
+++ b/src/EggRouter.ts
@@ -72,30 +72,11 @@ export class EggRouter extends Router {
     this.app = app;
   }
 
-  #formatRouteParams(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc | ResourcesController,
-    middlewares: (MiddlewareFunc | string | ResourcesController)[]) {
-    const options: RegisterOptions = {};
-    let path: string | RegExp;
-    if (typeof pathOrMiddleware === 'string' || pathOrMiddleware instanceof RegExp) {
-      // verb(method, name, path, ...middlewares)
-      path = pathOrMiddleware;
-      assert(typeof nameOrPath === 'string', 'route name should be string');
-      options.name = nameOrPath;
-    } else {
-      // verb(method, path, ...middlewares)
-      path = nameOrPath;
-      middlewares = [ pathOrMiddleware, ...middlewares ];
-    }
-    return {
-      path,
-      middlewares,
-      options,
-    };
-  }
-
-  verb(method: RouterMethod | RouterMethod[], nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  verb(method: RouterMethod | RouterMethod[],
+    nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middleware: (MiddlewareFunc | string)[]) {
-    const { path, middlewares, options } = this.#formatRouteParams(nameOrPath, pathOrMiddleware, middleware);
+    const { path, middlewares, options } = this._formatRouteParams(nameOrPath, pathOrMiddleware, middleware);
     if (typeof method === 'string') {
       method = [ method ];
     }
@@ -104,56 +85,64 @@ export class EggRouter extends Router {
   }
 
   // const METHODS = [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete', 'all' ];
-  head(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  head(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  head(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  head(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  head(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  head(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: (MiddlewareFunc | string)[]): Router {
     return this.verb('head', nameOrPath, pathOrMiddleware, ...middlewares);
   }
-  options(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  options(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  options(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  options(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  options(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  options(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: (MiddlewareFunc | string)[]): Router {
     return this.verb('options', nameOrPath, pathOrMiddleware, ...middlewares);
   }
-  get(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  get(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  get(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  get(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  get(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  get(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: (MiddlewareFunc | string)[]): Router {
     return this.verb('get', nameOrPath, pathOrMiddleware, ...middlewares);
   }
-  put(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  put(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  put(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  put(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  put(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  put(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: (MiddlewareFunc | string)[]): Router {
     return this.verb('put', nameOrPath, pathOrMiddleware, ...middlewares);
   }
-  patch(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  patch(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  patch(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  patch(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  patch(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  patch(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: (MiddlewareFunc | string)[]): Router {
     return this.verb('patch', nameOrPath, pathOrMiddleware, ...middlewares);
   }
-  post(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  post(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  post(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  post(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  post(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  post(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: (MiddlewareFunc | string)[]): Router {
     return this.verb('post', nameOrPath, pathOrMiddleware, ...middlewares);
   }
-  delete(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  delete(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  delete(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  delete(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  delete(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  delete(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: (MiddlewareFunc | string)[]): Router {
     return this.verb('delete', nameOrPath, pathOrMiddleware, ...middlewares);
   }
-  all(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  all(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router;
-  all(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc,
+  all(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  all(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
+  all(nameOrPath: string | RegExp | (string | RegExp)[],
+    pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
     ...middlewares: (MiddlewareFunc | string)[]): Router {
     return this.verb(methods, nameOrPath, pathOrMiddleware, ...middlewares);
   }
 
-  register(path: string | string[] | RegExp | RegExp[],
+  register(path: string | RegExp | (string | RegExp)[],
     methods: string[],
     middleware: MiddlewareFunc | string | (MiddlewareFunc | string | ResourcesController)[],
     opts?: RegisterOptions) {
@@ -213,7 +202,7 @@ export class EggRouter extends Router {
   resources(name: string, prefix: string, middleware: MiddlewareFunc, controller: string | ResourcesController): Router;
   resources(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc | ResourcesController,
     ...middleware: (MiddlewareFunc | string | ResourcesController)[]): Router {
-    const { path, middlewares, options } = this.#formatRouteParams(nameOrPath, pathOrMiddleware, middleware);
+    const { path, middlewares, options } = this._formatRouteParams(nameOrPath, pathOrMiddleware, middleware);
     // last argument is Controller object
     const controller = resolveController(middlewares.pop()!, this.app);
     for (const key in REST_MAP) {
diff --git a/src/Router.ts b/src/Router.ts
index b92527a..5400b0e 100644
--- a/src/Router.ts
+++ b/src/Router.ts
@@ -401,17 +401,26 @@ export class Router {
     methods: string[],
     middleware: MiddlewareFunc | MiddlewareFunc[],
     opts?: RegisterOptions): Layer | Layer[] {
-    opts = opts ?? {};
     // support array of paths
     if (Array.isArray(path)) {
       const routes: Layer[] = [];
       for (const p of path) {
-        const route = this.register(p, methods, middleware, opts) as Layer;
+        const route = this.#register(p, methods, middleware, opts);
         routes.push(route);
       }
       return routes;
     }
 
+    // create route
+    const route = this.#register(path, methods, middleware, opts);
+    return route;
+  }
+
+  #register(path: string | RegExp,
+    methods: string[],
+    middleware: MiddlewareFunc | MiddlewareFunc[],
+    opts?: RegisterOptions): Layer {
+    opts = opts ?? {};
     // create route
     const route = new Layer(path, methods, middleware, {
       end: opts.end === false ? opts.end : true,
@@ -478,12 +487,6 @@ export class Router {
    * 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
-   * @return {String|Error} string or error instance
    */
   url(name: string, params?: string | number | object,
     ...paramsOrOptions: (string | number | object | LayerURLOptions)[]): string | Error {
@@ -733,13 +736,6 @@ export class Router {
    * The [path-to-regexp](https://github.com/pillarjs/path-to-regexp) module is
    * used to convert paths to regular expressions.
    *
-   * @name get|put|post|patch|delete|del
-   * @memberof module:koa-router.prototype
-   * @param {String} method http method
-   * @param {String} nameOrPath http path
-   * @param {Function=} pathOrMiddleware route middleware(s)
-   * @param {Function} middlewares middlewares
-   * @return {Router} Router instance
    */
   verb(method: string | string[],
     nameOrPath: string | RegExp | (string | RegExp)[],
diff --git a/test/EggRouter.test.ts b/test/EggRouter.test.ts
index 1dec08e..bdef34a 100644
--- a/test/EggRouter.test.ts
+++ b/test/EggRouter.test.ts
@@ -55,6 +55,31 @@ describe('test/EggRouter.test.ts', () => {
     assert(router.stack[1].stack.length === 1);
   });
 
+  it('should app.verb([url1, url2], controller) work', () => {
+    const app = {
+      controller: {
+        async foo() { return; },
+        hello: {
+          world() { return; },
+        },
+      },
+    };
+
+    const router = new EggRouter({}, app);
+    router.get([ '/foo', '/bar' ], app.controller.foo);
+    router.post('/hello/world', app.controller.hello.world);
+
+    assert(router.stack[0].path === '/foo');
+    assert.deepEqual(router.stack[0].methods, [ 'HEAD', 'GET' ]);
+    assert(router.stack[0].stack.length === 1);
+    assert(router.stack[1].path === '/bar');
+    assert.deepEqual(router.stack[1].methods, [ 'HEAD', 'GET' ]);
+    assert(router.stack[2].stack.length === 1);
+    assert(router.stack[2].path === '/hello/world');
+    assert.deepEqual(router.stack[2].methods, [ 'POST' ]);
+    assert(router.stack[2].stack.length === 1);
+  });
+
   it('should app.verb(name, url, controller) work', () => {
     const app = {
       controller: {

From 38b0bad24a3414b0aa75c994542ceef865eac4cc Mon Sep 17 00:00:00 2001
From: fengmk2 <suqian.yf@antgroup.com>
Date: Tue, 11 Jun 2024 14:40:25 +0800
Subject: [PATCH 11/13] f

---
 README.md | 66 +++++++++++++++++++++++++++----------------------------
 1 file changed, 33 insertions(+), 33 deletions(-)

diff --git a/README.md b/README.md
index 7595ad4..0967490 100644
--- a/README.md
+++ b/README.md
@@ -49,14 +49,14 @@ Create a new router.
 **Example**
 Basic usage:
 
-```javascript
-var Koa = require('koa');
-var Router = require('@eggjs/router');
+```ts
+import Koa from '@eggjs/koa';
+import Router from '@eggjs/router';
 
-var app = new Koa();
-var router = new Router();
+const app = new Koa();
+const router = new Router();
 
-router.get('/', (ctx, next) => {
+router.get('/', async (ctx, next) => {
   // ctx.router available
 });
 
@@ -77,7 +77,7 @@ where **verb** is one of the HTTP verbs such as `router.get()` or `router.post()
 
 Additionaly, `router.all()` can be used to match against all methods.
 
-```javascript
+```ts
 router
   .get('/', (ctx, next) => {
     ctx.body = 'Hello World!';
@@ -109,7 +109,7 @@ Query strings will not be considered when matching requests.
 Routes can optionally have names. This allows generation of URLs and easy
 renaming of URLs during development.
 
-```javascript
+```ts
 router.get('user', '/users/:id', (ctx, next) => {
  // ...
 });
@@ -122,7 +122,7 @@ router.url('user', 3);
 
 Multiple middleware may be given:
 
-```javascript
+```ts
 router.get(
   '/users/:id',
   (ctx, next) => {
@@ -142,9 +142,9 @@ router.get(
 
 Nesting routers is supported:
 
-```javascript
-var forums = new Router();
-var posts = new Router();
+```ts
+const forums = new Router();
+const posts = new Router();
 
 posts.get('/', (ctx, next) => {...});
 posts.get('/:pid', (ctx, next) => {...});
@@ -158,8 +158,8 @@ app.use(forums.routes());
 
 Route paths can be prefixed at the router level:
 
-```javascript
-var router = new Router({
+```ts
+const router = new Router({
   prefix: '/users'
 });
 
@@ -171,7 +171,7 @@ router.get('/:id', ...); // responds to "/users/:id"
 
 Named route parameters are captured and added to `ctx.params`.
 
-```javascript
+```ts
 router.get('/:category/:title', (ctx, next) => {
   console.log(ctx.params);
   // => { category: 'programming', title: 'how-to-node' }
@@ -215,7 +215,7 @@ sequentially, requests start at the first middleware and work their way
 
 **Example**
 
-```javascript
+```ts
 // session middleware will run before authorize
 router
   .use(session())
@@ -244,7 +244,7 @@ Set the path prefix for a Router instance that was already initialized.
 
 **Example**
 
-```javascript
+```ts
 router.prefix('/things/:thing_id')
 ```
 
@@ -267,12 +267,12 @@ with `405 Method Not Allowed` and `501 Not Implemented` as appropriate.
 
 **Example**
 
-```javascript
-var Koa = require('koa');
-var Router = require('egg-router');
+```ts
+import Koa from '@eggjs/koa';
+import Router from '@eggjs/router';
 
-var app = new Koa();
-var router = new Router();
+const app = new Koa();
+const router = new Router();
 
 app.use(router.routes());
 app.use(router.allowedMethods());
@@ -280,13 +280,13 @@ app.use(router.allowedMethods());
 
 **Example with [Boom](https://github.com/hapijs/boom)**
 
-```javascript
-var Koa = require('koa');
-var Router = require('egg-router');
-var Boom = require('boom');
+```ts
+import Koa from '@eggjs/koa';
+import Router from '@eggjs/router';
+import Boom from 'boom';
 
-var app = new Koa();
-var router = new Router();
+const app = new Koa();
+const router = new Router();
 
 app.use(router.routes());
 app.use(router.allowedMethods({
@@ -310,7 +310,7 @@ router.redirect('/login', 'sign-in');
 
 This is equivalent to:
 
-```javascript
+```ts
 router.all('/login', ctx => {
   ctx.redirect('/sign-in');
   ctx.status = 301;
@@ -354,7 +354,7 @@ Generate URL for route. Takes a route name and map of named `params`.
 
 **Example**
 
-```javascript
+```ts
 router.get('user', '/users/:id', (ctx, next) => {
   // ...
 });
@@ -393,7 +393,7 @@ validation.
 
 **Example**
 
-```javascript
+```ts
 router
   .param('user', (id, ctx, next) => {
     ctx.user = users[id];
@@ -429,8 +429,8 @@ Generate URL from url pattern and given `params`.
 
 **Example**
 
-```javascript
-var url = Router.url('/users/:id', {id: 1});
+```ts
+const url = Router.url('/users/:id', {id: 1});
 // => "/users/1"
 
 const url = Router.url('/users/:id', {id: 1}, {query: { active: true }});

From ad4cd64acc9b08d88f0bd5d94f458cb187d6c054 Mon Sep 17 00:00:00 2001
From: fengmk2 <suqian.yf@antgroup.com>
Date: Tue, 11 Jun 2024 15:03:44 +0800
Subject: [PATCH 12/13] f

---
 bench/run        |  2 +-
 bench/server.cjs | 10 +++++-----
 package.json     |  5 +++--
 3 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/bench/run b/bench/run
index 9cd8782..b1892f8 100755
--- a/bench/run
+++ b/bench/run
@@ -8,7 +8,7 @@ export PORT=3333
 
 host="http://localhost:$PORT"
 
-node "$(dirname $0)/server.js" &
+node "$(dirname $0)/server.cjs" &
 
 pid=$!
 
diff --git a/bench/server.cjs b/bench/server.cjs
index 4d4dbdf..adb5e07 100644
--- a/bench/server.cjs
+++ b/bench/server.cjs
@@ -1,8 +1,8 @@
 /* eslint-disable @typescript-eslint/no-var-requires */
-const Koa = require('@eggjs/koa');
-const Router = require('../dist/commonjs');
+const { Application } = require('@eggjs/koa');
+const { Router } = require('../dist/commonjs');
 
-const app = new Koa();
+const app = new Application();
 const router = new Router();
 
 const ok = ctx => {
@@ -35,8 +35,8 @@ 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.nest('/grandchild', grandchild);
-  router.nest(`/${i}/child`, child);
+  // child.use('/grandchild', grandchild);
+  // router.use(`/${i}/child`, child);
 }
 
 if (process.env.DEBUG) {
diff --git a/package.json b/package.json
index 149e16b..a56a0bd 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
     "utility": "^2.1.0"
   },
   "devDependencies": {
-    "@eggjs/koa": "^2.18.0",
+    "@eggjs/koa": "^2.18.1",
     "@eggjs/tsconfig": "^1.3.3",
     "@types/koa-compose": "^3.2.8",
     "@types/methods": "^1.1.4",
@@ -57,7 +57,8 @@
     "preci": "npm run lint && npm run prepublishOnly",
     "ci": "egg-bin cov",
     "contributor": "git-contributor",
-    "prepublishOnly": "tshy && tshy-after"
+    "prepublishOnly": "tshy && tshy-after",
+    "bench": "cd bench && make"
   },
   "license": "MIT",
   "files": [

From edf455d3e2c6bb8ae2328701cbd113a045f84213 Mon Sep 17 00:00:00 2001
From: fengmk2 <suqian.yf@antgroup.com>
Date: Tue, 11 Jun 2024 15:13:00 +0800
Subject: [PATCH 13/13] f

---
 test/EggRouter.test.ts |  7 +++++++
 test/Router.test.ts    | 11 +++++++++++
 2 files changed, 18 insertions(+)

diff --git a/test/EggRouter.test.ts b/test/EggRouter.test.ts
index bdef34a..353df64 100644
--- a/test/EggRouter.test.ts
+++ b/test/EggRouter.test.ts
@@ -53,6 +53,13 @@ describe('test/EggRouter.test.ts', () => {
     assert(router.stack[1].path === '/hello/world');
     assert.deepEqual(router.stack[1].methods, [ 'POST' ]);
     assert(router.stack[1].stack.length === 1);
+
+    router.head('/foo-head', app.controller.foo);
+    router.options('/foo-options', app.controller.foo);
+    router.put('/foo-put', app.controller.foo);
+    router.patch('/foo-patch', app.controller.foo);
+    router.delete('/foo-delete', app.controller.foo);
+    router.all('/foo-all', app.controller.foo);
   });
 
   it('should app.verb([url1, url2], controller) work', () => {
diff --git a/test/Router.test.ts b/test/Router.test.ts
index d32f68d..b10a137 100644
--- a/test/Router.test.ts
+++ b/test/Router.test.ts
@@ -956,6 +956,17 @@ describe('test/lib/router.test.js', () => {
         .expect(301);
       assert.equal(res.headers.location, '/sign-up-form');
     });
+
+    it('registers redirect not exists routes', () => {
+      const router = new Router();
+      assert(typeof router.redirect === 'function');
+      assert.throws(() => {
+        router.redirect('source-not-exists', '/destination', 302);
+      }, /Error: No route found for name: source-not-exists/);
+      assert.throws(() => {
+        router.redirect('/source', 'destination-not-exists');
+      }, /Error: No route found for name: destination-not-exists/);
+    });
   });
 
   describe('Router#route()', () => {