koa-router implementation principle

koa router implementation principle

Two purposes of this article

  1. Understanding the use of path-to-regexp
  2. koa-router source parsing


Introduction to path-to-regexp usage.

How can it be used to match identified routes?

Think about what we can do if we want to identify routes?

The most intuitive is definitely a path string match

'/string' => '/string'

We can do some feedback when the routes match/string exactly.For example, execute a callback, and so on.

We can also take advantage of the regular matching feature

In this way, the sub-matching pattern is obviously more versatile and has more matching paths

For example, for path:

// => '/string/try/xixi'

path-to-regexp is one such tool

Consider that if we want to parse and match paths, we need to write our own regular expressions.This achieves the matching effect.

Can I write?

Sure, but it's too time consuming.

path-to-regexp can help us do this easily.

Introduction to some api of path-to-regexp

how to use it ???

Primary api

const pathToRegexp = require('path-to-regexp')

// pathToRegexp(path, keys?, options?)
// pathToRegexp.parse(path)
// pathToRegexp.compile(path)
// pathToRegexp(path, keys?, options?)

// path can be a string/string array/regular expression
// Keys holds the array of keys found in the path
// options are populations of some matching rules such as whether they are fully matched splitters, etc.

path-to-regexp api demo

//a demo

If we want to match some key values properly



How do we achieve this

The front is a perfect match, and the back is a regular grouping to extract values



Find matching regular string returns an array/no value returns a null

This is what pathToRegexp does.Generate the required regular expression matches.There are some packaging operations, of course, but that's what it's all about.


    option ?
        Indicates availability or absence
        *Representatives can come anywhere
        +for one or more
        Look closely and you can see that these words are almost identical to the quantifiers in the regular
        You can also store keys by sequence subscripts when matching unnamed parameters
        Regular expressions are also supported

parse method

    Generate a matching tokens array for path
    That is the keys array above
    Method applies to string type
Compile method

    Passing in a path with compile returns a function that can be populated to generate a value that matches the path
    pathToRegexp.compile('/user/:id')({id: 123}) => "/user/123"
    Applicable to strings
pathToRegexp.tokensToRegExp(tokens, keys?, options?) 


You can see by name 

A regular expression that converts an array of tokens

A function that converts a tokens array into a compile method

Steps for using one stroke

pathToRegexp =Return=> regexp

parse =analysis=> path =matching tokens=> keys token

compile => path => generator function => value => full path string


I don't know if you've ever used koa-router

notic: Notice the current koa-router maintenance permission change issue

The router implementation is also essentially a Regular-based access path matching.

If koa native code is used


Match Path/simple returns a body string with body {name:'zwkang'}

A simple example, such as

Suppose we match routes using a simple middleware match ctx.url

app.use(async (ctx, next) => {
    const url = ctx.url
    if(/^\/simple$/i.test(url)) {
        ctx.body = {
            name: 'ZWkang'
    } else {
        ctx.body = {
            errorCode: 404,
            message: 'NOT FOUND'
        ctx.status = 404
    return await next()

//Test Code
describe('use normal koa path', () => {
    it('use error path', (done) => {
        .end(function (err, res) {
            if (err) return done(err);
            expect(res.body).to.have.property('errorCode', 404)
    it('use right path', (done) => {
        .end(function (err, res) {
            if (err) return done(err);
            expect(res.body).to.have.property('name', 'ZWkang')

The above pattern of url implementation by ourselves is the same. Single matching, if multiple matching, even matching parameters, requires regular writing to be considered.

Disadvantages, simpler, simpler setting method, weak function

If we use koa-router

// A simple use
it('simple use should work', (done) => {
    router.get('/simple', (ctx, next) => {
        ctx.body = {
            path: 'simple'
      .end(function (err, res) {
        if (err) return done(err);
        expect(res.body).to.have.property('path', 'simple');

Off-topic: app.callback()

Some explanations of the test code above

callback is koa's operating mechanism.What does the method represent?Represents the process of its setup

And our common listen method is actually the only step that calls http.createServer(app.callback())

Let's see what this koa-router actually does


As we can see from the simple examples above, we can understand the operating mechanism of koa and the internal middleware processing mode.

Start with demo for analysis

Instance methods called when koa is called include

router.allowedMethods ===> router.routes ===> router.get

Consider that because it's a koa, use call, we can be sure it's a standard koa middleware pattern

The function returned is similar to

async (ctx, next) => {
    // Processing routing logic
    // Processing business logic

The opening comments of the source code give us some basic usage

We can make a simple extraction

router.verb() specifies the corresponding function based on the http method

For example, router.get().post().put()

The.All method supports all http methods

When a route matches, ctx._matchedRoute can get the route here, and if it is named, it can get the route name ctx._matchedRouteName here

querystring(?xxxx) is not considered when requesting matches

Allow use of named functions

Quickly locate routes during development

 * router.get('user', '/users/:id', (ctx, next) => {
 *  // ...
 * });
 * router.url('user', 3);
 * // => "/users/3"

Allow multiple routes

 * 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" }
 *   }
 * );

Allow Nested Routing

 * 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());

Allow routing prefix matching

 var router = new Router({
    prefix: '/users'
 router.get('/', ...); // responds to "/users"
 router.get('/:id', ...); // responds to "/users/:id"

Capture named parameters to add to ctx.params

 router.get('/:category/:title', (ctx, next) => {
    // => { category: 'programming', title: 'how-to-node' }

Code Integration Analysis

Some clever coding

  1. Separation of responsibilities, upper-level routers do http-level method status es, and routers middlewares-related processing.Low-level Layer.js focuses on routing path processing
  2. middlerware design

You might want to start with a layer file.


As mentioned earlier, this file is primarily used to handle operations on the path-to-regexp Library

There are only about 300 lines in the file, so the direct intercept method is explained in detail.

layer constructor

function Layer(path, methods, middleware, opts) {
  this.opts = opts || {};
  this.name = this.opts.name || null; // Named Route
  this.methods = []; // Allow methods
  // [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
  this.paramNames = [];
  this.stack = Array.isArray(middleware) ? middleware : [middleware]; // middleware heap
  // Initialization parameters
  // Tips: the second parameter of forEach can pass this 
  // The forEach push array can be used later to determine the end element using the array [l-1]
  // The push method returns the number of elements after the array push

  // External method parameter passed in
  methods.forEach(function(method) {
    var l = this.methods.push(method.toUpperCase());
    // If a GET request supports a HEAD request
    if (this.methods[l-1] === 'GET') {
  }, this);

  // ensure middleware is a function
  // Ensure that each middleware is a function
  this.stack.forEach(function(fn) {
    var type = (typeof fn);
    if (type !== 'function') {
      throw new Error(
        methods.toString() + " `" + (this.opts.name || path) +"`: `middleware` "
        + "must be a function, not `" + type + "`"
  }, this);
  // Route
  this.path = path;
  // Generating regular expressions for paths using pathToRegExp
  // Arrays associated with params fall back into our this.paramNames
  // this.regexp An array generated for cutting
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);

  debug('defined route %s %s', this.methods, this.opts.prefix + this.path);

We can focus on input and output.

Input: path, methods, middleware, opts

Output: Object properties include (opts, name, methods, paramNames, stack, path, regexp)

We've said before that layer s are processed to determine matches based on route path s, which is important for the connection library path-to-regexp.

The stack should match the middleware passed in.The stack is an array, so you can see that our path's corresponding route s allow more than one.

Let's focus next

What encapsulation does koa-router handle for us, based on path-to-regexp combining middleware with its own needs

The mount methods on the prototype chain are


// Get Routing Parameter Key-Value Pairs
Layer.prototype.params = function (path, captures, existingParams) {
  var params = existingParams || {};

  for (var len = captures.length, i=0; i<len; i++) {
    if (this.paramNames[i]) { // Get the corresponding capture group
      var c = captures[i]; // Get parameter values
      params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c; 
      // Fill in key-value pairs
  // Returns a parameter key-value pair object
  return params;

When the constructor is initialized, we generate this.regexp by passing in this.paramNames to fill out the param parsed from the path

Input: Path, Capture Group, Existing Parameter Group
Output: A parameter key-value pair object

Processing is very common.Because params correspond to captures in location.So you can cycle directly.


// Determine whether to match
Layer.prototype.match = function (path) {
  return this.regexp.test(path);

The first thing to look at is the input and return values

Input: path

Output: whether a boolean matches

We can see that this.regexp is an attribute value, proving that we have the ability to change this.regexp at any time to affect the return value of this function


// Return parameter value
Layer.prototype.captures = function (path) {
  if (this.opts.ignoreCaptures) return []; // Ignore capture and return null

  // match returns an array of matching results
  // From the regular rule, the result is a perfect match.
   * eg: 
   *    var test = []
   *    pathToRegExp('/:id/name/(.*?)', test)
   *    /^\/((?:[^\/]+?))\/name\/((?:.*?))(?:\/(?=$))?$/i
   *    '/xixi/name/ashdjhk'.match(/^\/((?:[^\/]+?))\/name\/((?:.*?))(?:\/(?=$))?$/i)
   *    ["/xixi/name/ashdjhk", "xixi", "ashdjhk"]

  return path.match(this.regexp).slice(1); // [value, value .....]

Input: path path path

Output: Capture array

Return the entire capture group content


Layer.prototype.url = function(params, options) {
  var args = params;
  var url = this.path.replace(/\(\.\*\)/g, "");
  var toPath = pathToRegExp.compile(url); //
  var replaced;

  if (typeof params != "object") {
    args = Array.prototype.slice.call(arguments);
    if (typeof args[args.length - 1] == "object") {
      options = args[args.length - 1];
      args = args.slice(0, args.length - 1);
  var tokens = pathToRegExp.parse(url);
  var replace = {};

  if (args instanceof Array) {
    for (var len = tokens.length, i = 0, j = 0; i < len; i++) {
      if (tokens[i].name) replace[tokens[i].name] = args[j++];
  } else if (tokens.some(token => token.name)) {
    replace = params; // replace = params
  } else {
    options = params; // options = params

  replaced = toPath(replace); // By default, replace is the default incoming key-value pair//match followed by the full url

  if (options && options.query) {
    // Is there query
    var replaced = new uri(replaced); //
    replaced.search(options.query); //Add query routing query
    return replaced.toString();

  return replaced; // Return URL String

url method of layer instance

In fact, an example is/name/:id

After parsing, we get a params object of {id: xxx}

Can we reverse the actual url based on / name/:id and params objects?

This url method provides this capability.


Layer.prototype.param = function(param, fn) {
  var stack = this.stack;
  var params = this.paramNames;
  var middleware = function(ctx, next) {
    return fn.call(this, ctx.params[param], ctx, next);
  middleware.param = param;

  var names = params.map(function(p) {
    return String(p.name);
  var x = names.indexOf(param); // Get index

  if (x > -1) {
    stack.some(function(fn, i) {
      // param handlers are always first, so when we find an fn w/o a param property, stop here
      // if the param handler at this part of the stack comes after the one we are adding, stop here

      // Two strategies
      // 1. The param processor is always at the top, and the current fn.param does not exist.Insert [a, b] mid => [mid, a, b]
      // 2. [mid, a, b] mid2 => [mid, mid2, a, b] guarantees the order of params
      // Ensure before normal Middleware
      // Ensure that the order is params
      if (!fn.param || names.indexOf(fn.param) > x) {
        // Injecting middleware at the current time
        stack.splice(i, 0, middleware);
        return true; // Stop some iteration.
  return this;

The purpose of this method is to add a processor for a single param to the current stack

It's actually an operation on the layer's stack


Layer.prototype.setPrefix = function(prefix) {
  // Calling setPrefix is equivalent to resetting some of the layer's constructs
  if (this.path) {
    this.path = prefix + this.path;
    this.paramNames = [];
    this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);

  return this;

Prefix the current path and reset some of the current instance properties


function safeDecodeURIComponent(text) {
  try {
    return decodeURIComponent(text);
  } catch (e) {
    return text;

Ensure that safeDecodeURIComponent does not throw any errors

Layer concludes.

The stack of layer s mainly stores the actual middleware[s].

The main function is to design for pathToRegexp.
Provides the ability to make calls to higher-level routers.


Router mainly responds to the upper koa framework (ctx, status, etc.) and links the lower layer instances.

Router Constructor

function Router(opts) {
    // Automatic new
  if (!(this instanceof Router)) {
    return new Router(opts);

  this.opts = opts || {};
  // The method is used to check the allowedMethod that follows
  this.methods = this.opts.methods || [
  ]; // Initialize http method

  this.params = {}; // Parameter key-value pairs
  this.stack = []; // Store Routing Instances
methods.forEach(function(method) {
  // Attach all http method methods to the prototype
  Router.prototype[method] = function(name, path, middleware) {
    var middleware;
    // Compatibility parameters
    // Allow path to be a string or regular expression
    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;
    // Register with the current instance
    // It's mainly a way to set up a generic install middleware.(mark. tag: function)
    this.register(path, [method], middleware, {
      name: name
    // call chaining
    return this;

Register Router prototype

Methods for HTTP methods, such as Router.prototype.get = xxx

It's easier and more accurate when we use instances

router.get('name', path, cb)

Middlewares can obviously be multiple here.For example, router.get(name, path, cb)

We can notice that the main thing here is to call another method

register method.And we can keep an eye on this method.Much like Layer instance initialization.

With doubt, we can enter the register method.

register method

Router.prototype.register = function(path, methods, middleware, opts) {
  opts = opts || {};

  var router = this;
  var stack = this.stack;
  if (Array.isArray(path)) {
    path.forEach(function(p) {
      router.register.call(router, p, methods, middleware, opts);
    return this;

  var route = new Layer(path, methods, middleware, {
    end: opts.end === false ? opts.end : true, // Need to be explicitly declared as end
    name: opts.name, // The name of the route
    sensitive: opts.sensitive || this.opts.sensitive || false, // Case Sensitive Regular Plus i
    strict: opts.strict || this.opts.strict || false, // Non-capture grouping plus (?:)
    prefix: opts.prefix || this.opts.prefix || "", // Prefix character
    ignoreCaptures: opts.ignoreCaptures || false // Use Ignore Capture for layer

  if (this.opts.prefix) {

  // add parameter middleware
  // Add Parameter Middleware
  Object.keys(this.params).forEach(function(param) {
    route.param(param, this.params[param]);
  }, this);
  // Current Router instance stack push single layer instance

  return route;

We can see that the entire register method is designed to register a single path.

Call the register method on forEach for multipaths.This is not uncommon in koa-router implementations.

Looking at the register method, our doubts are confirmed, and it turns out that most of the input is used to initialize the layer instance.

After initializing the layer instance, we place it in the stack under the router instance.

Processing judgment based on some opts.Not much is harmless and elegant.

That way we know how to use register s.

  1. Initialize layer instance
  2. Register it with the router instance.

We know when we call the router instance.

To use middleware, we often need to complete two steps

  1. use(router.routes())
  2. use(router.allowedMethods())

We know that a minimalist form of middleware call is always

app.use(async (ctx, next) => {
    await next()

Whether we are koa-body or koa-router

Incoming app.use is always one

async (ctx, next) => {
    await next()

Such a function meets the requirements of koa middleware.

With this in mind

We can explore the routes approach.

routes Prototype Method

Router.prototype.routes = Router.prototype.middleware = function() {
  var router = this;

  var dispatch = function dispatch(ctx, next) {
    debug("%s %s", ctx.method, ctx.path);
    // Get Path
    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    // matched has already been processed to get layer object hosting
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;
    // Consider multiple router instances
    if (ctx.matched) {
      // Because matched is always an array
      // apply here is similar to concat
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      // Matched Path
      ctx.matched = matched.path;
    // Current Route
    ctx.router = router;
    // If a matching route exists
    if (!matched.route) return next();
    // layer whose method and path match
    var matchedLayers = matched.pathAndMethod;
    // Last layer
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1];
    ctx._matchedRoute = mostSpecificLayer.path;

    // If a layer has a name
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    // Matching layer s perform compose operations

    // update capture params routerName, etc.

    // For example, if we use multiple routes.
    // => ctx.capture, ctx.params, ctx.routerName => layer Stack[s]
    // => ctx.capture, ctx.params, ctx.routerName => next layer Stack[s]
    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.routerName = layer.name;
        return next();
      return memo.concat(layer.stack);
    }, []);

    return compose(layerChain)(ctx, next);

  dispatch.router = this;

  return dispatch;

We know that the essence of route matching is that the actual route matches the defined route.

So the middleware generated by routes is actually thinking about doing this matching.

From the return value we can see that

=> dispatch method.

This dispacth method is actually the simplest way we've said before.

function dispatch(ctx, next) {}

It can be said that there is little difference.

We know that stack currently stores multiple layer instances.

And based on the matching of paths, we can see that

A back-end path, which can be simply divided into http methods, matches the path definition.

For example: /name/:id

A request comes at this time/name/3

Is it a match?(params = {id: 3})

But what if the request method is a get? Define this / name/:id if it is a post.

At this point, although the paths match, they do not match exactly.

Prototype method match

Router.prototype.match = function(path, method) {
  var layers = this.stack;
  var layer;
  var matched = {
    path: [],
    pathAndMethod: [],
    route: false

  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i];

    debug("test %s %s", layer.path, layer.regexp);

    if (layer.match(path)) {
      //If the path matches
      // Press layer into matched

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        // Check Method
        // layer is pressed into both paths and methods
        if (layer.methods.length) matched.route = true;
        // Prove that there are no supported methods.Skip middleware processing after route is true

  return matched;

Take a look at this match method.

Judge layaer in stack.

In the returned matched object

Path property: Simply path matching.

Path AndMethod property: Just http method matches path.

route property: The length of the method requiring a layer is not zero (there is a defined method).)

So in dispatch we start with

ctx.matched = matched.path

Get a path-matching layer

The actual middleware handles layer with http method and path matching

In this case.In fact, the so-called middleware is an array

It may be stacked in a multidimensional or one-dimensional manner.

If a route matches

ctx._matchedRoute represents its path.

Here ctx._matchedRoute is the last layer of the method and path matching array.

Believe that the last one you know why.Multiple paths, except for the current process, always return to the last one in the next middleware process.

Finally, the layer s that match are grouped together

For example, if there are multiple layers, the layers also have multiple stack s

// For example, if we use multiple routes.
// => ctx.capture, ctx.params, ctx.routerName => layer Stack[?s]
// => ctx.capture, ctx.params, ctx.routerName => next layer Stack[?s]

The run order will be as shown above
This is equivalent to flattening stack s for multiple layer instances and adding a ctx attribute to use it before each layer instance.

Finally use compose to bring this flattened array together.

In fact, here we can notice that the so-called middleware is just an array.

But it's a good idea to use the ctx attribute before each layer instance here.

Operations on middleware, such as prefix, etc.Is the constant adjustment of the internal stack position property.

allowedMethods method

Router.prototype.allowedMethods = function(options) {
  options = options || {};
  var implemented = this.methods;
  // Returns a middleware for app.use registration.
  return function allowedMethods(ctx, next) {
    return next().then(function() {
      var allowed = {};
      // Judge ctx.status or status code to 404
      console.log(ctx.matched, ctx.method, implemented);

      if (!ctx.status || ctx.status === 404) {
        // ctx.matched generated by routes method
        // Is the filtered layer matching group
        ctx.matched.forEach(function(route) {
          route.methods.forEach(function(method) {
            allowed[method] = method;

        var allowedArr = Object.keys(allowed);
        // Route Matching Implemented
        if (!~implemented.indexOf(ctx.method)) {
          // Bit Operator ~(-1) === 0! 0 == true
          // options parameter throw throw throw throws an error if true
          // This allows the upper middle price to be handled
          // The default is to throw an HttpError
          if (options.throw) {
            var 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 {
            // Otherwise run out 501
            // 501=>Server unimplemented method
            ctx.status = 501;
            ctx.set("Allow", allowedArr.join(", "));
          // If permitted
        } else if (allowedArr.length) {
          // Operate on options requests.
          // options requests are similar to get requests, but requests have no body but only a head.
          // Common Terms Query Operation
          if (ctx.method === "OPTIONS") {
            ctx.status = 200;
            ctx.body = "";
            ctx.set("Allow", allowedArr.join(", "));
          } else if (!allowed[ctx.method]) {
            // If methods are allowed
            if (options.throw) {
              var 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 {
              // 405 Method Not Allowed
              ctx.status = 405;
              ctx.set("Allow", allowedArr.join(", "));

This method adds these state controls by default to our routing middleware 404 405 501.

We can also do this in high-level middleware.

It is also common to use the bitwise operator + indexOf.

Full text summary

So far, the koa-router source code has been basically parsed.

Although there are many ways to source Router that are not written out in this article, most of them are method connections that provide layer instances to the upper level. Welcome to the github link to view it from the source.

Overall, there may be a lot of points that can be absorbed.

If you have read the whole article.

  1. I'm sure you should be more comfortable with koa middleware.
  2. I'm sure you have a good idea of how to implement the source architecture of koa-router.
  3. Learn how to read the source code, build test cases, and learn about input and participating output.

My blog zwkang.com

Source Address (Note Parser) koa-router Branch

Keywords: node.js Attribute Programming github

Added by paulspoon on Fri, 23 Aug 2019 19:03:29 +0300