├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bench ├── Makefile ├── run └── server.js ├── history.md ├── lib ├── README_tpl.hbs ├── layer.js └── router.js ├── package.json └── test ├── index.js └── lib ├── layer.js └── router.js /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 18 | 19 | node.js version: 20 | 21 | npm/yarn and version: 22 | 23 | `koa-router` version: 24 | 25 | `koa` version: 26 | 27 | #### Code sample: 28 | 29 | 35 | 36 | ```js 37 | 38 | ``` 39 | 40 | #### Expected Behavior: 41 | 42 | 43 | 44 | #### Actual Behavior: 45 | 46 | 47 | 48 | #### Additional steps, HTTP request details, or to reproduce the behavior or a test case: 49 | 50 | 51 | ```js 52 | ``` 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | package-lock.json 34 | yarn.lock 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | .env* 42 | !.env.test 43 | 44 | .DS_Store 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "7" 5 | - "8" 6 | notifications: 7 | email: 8 | on_success: never 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alexander C. Mingoia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # koa-router 2 | 3 | [![NPM version](https://img.shields.io/npm/v/koa-router.svg?style=flat)](https://npmjs.org/package/koa-router) [![NPM Downloads](https://img.shields.io/npm/dm/koa-router.svg?style=flat)](https://npmjs.org/package/koa-router) [![Node.js Version](https://img.shields.io/node/v/koa-router.svg?style=flat)](http://nodejs.org/download/) [![Build Status](https://img.shields.io/travis/alexmingoia/koa-router.svg?style=flat)](http://travis-ci.org/alexmingoia/koa-router) [![Gitter Chat](https://img.shields.io/badge/gitter-join%20chat-1dce73.svg?style=flat)](https://gitter.im/alexmingoia/koa-router/) 4 | 5 | > Router middleware for [koa](https://github.com/koajs/koa) 6 | 7 | * Express-style routing using `app.get`, `app.put`, `app.post`, etc. 8 | * Named URL parameters. 9 | * Named routes with URL generation. 10 | * Responds to `OPTIONS` requests with allowed methods. 11 | * Support for `405 Method Not Allowed` and `501 Not Implemented`. 12 | * Multiple route middleware. 13 | * Multiple routers. 14 | * Nestable routers. 15 | * ES7 async/await support. 16 | 17 | ## Migrating to 7 / Koa 2 18 | 19 | - The API has changed to match the new promise-based middleware 20 | signature of koa 2. See the 21 | [koa 2.x readme](https://github.com/koajs/koa/tree/2.0.0-alpha.3) for more 22 | information. 23 | - Middleware is now always run in the order declared by `.use()` (or `.get()`, 24 | etc.), which matches Express 4 API. 25 | 26 | ## Installation 27 | 28 | Install using [npm](https://www.npmjs.org/): 29 | 30 | ```sh 31 | npm install koa-router 32 | ``` 33 | 34 | ## API Reference 35 | 36 | * [koa-router](#module_koa-router) 37 | * [Router](#exp_module_koa-router--Router) ⏏ 38 | * [new Router([opts])](#new_module_koa-router--Router_new) 39 | * _instance_ 40 | * [.get|put|post|patch|delete|del](#module_koa-router--Router+get|put|post|patch|delete|del) ⇒ Router 41 | * [.routes](#module_koa-router--Router+routes) ⇒ function 42 | * [.use([path], middleware)](#module_koa-router--Router+use) ⇒ Router 43 | * [.prefix(prefix)](#module_koa-router--Router+prefix) ⇒ Router 44 | * [.allowedMethods([options])](#module_koa-router--Router+allowedMethods) ⇒ function 45 | * [.redirect(source, destination, [code])](#module_koa-router--Router+redirect) ⇒ Router 46 | * [.route(name)](#module_koa-router--Router+route) ⇒ Layer | false 47 | * [.url(name, params, [options])](#module_koa-router--Router+url) ⇒ String | Error 48 | * [.param(param, middleware)](#module_koa-router--Router+param) ⇒ Router 49 | * _static_ 50 | * [.url(path, params)](#module_koa-router--Router.url) ⇒ String 51 | 52 | 53 | 54 | ### Router ⏏ 55 | **Kind**: Exported class 56 | 57 | 58 | #### new Router([opts]) 59 | Create a new router. 60 | 61 | 62 | | Param | Type | Description | 63 | | --- | --- | --- | 64 | | [opts] | Object | | 65 | | [opts.prefix] | String | prefix router paths | 66 | 67 | **Example** 68 | Basic usage: 69 | 70 | ```javascript 71 | var Koa = require('koa'); 72 | var Router = require('koa-router'); 73 | 74 | var app = new Koa(); 75 | var router = new Router(); 76 | 77 | router.get('/', (ctx, next) => { 78 | // ctx.router available 79 | }); 80 | 81 | app 82 | .use(router.routes()) 83 | .use(router.allowedMethods()); 84 | ``` 85 | 86 | 87 | #### router.get|put|post|patch|delete|del ⇒ Router 88 | Create `router.verb()` methods, where *verb* is one of the HTTP verbs such 89 | as `router.get()` or `router.post()`. 90 | 91 | Match URL patterns to callback functions or controller actions using `router.verb()`, 92 | where **verb** is one of the HTTP verbs such as `router.get()` or `router.post()`. 93 | 94 | Additionaly, `router.all()` can be used to match against all methods. 95 | 96 | ```javascript 97 | router 98 | .get('/', (ctx, next) => { 99 | ctx.body = 'Hello World!'; 100 | }) 101 | .post('/users', (ctx, next) => { 102 | // ... 103 | }) 104 | .put('/users/:id', (ctx, next) => { 105 | // ... 106 | }) 107 | .del('/users/:id', (ctx, next) => { 108 | // ... 109 | }) 110 | .all('/users/:id', (ctx, next) => { 111 | // ... 112 | }); 113 | ``` 114 | 115 | When a route is matched, its path is available at `ctx._matchedRoute` and if named, 116 | the name is available at `ctx._matchedRouteName` 117 | 118 | Route paths will be translated to regular expressions using 119 | [path-to-regexp](https://github.com/pillarjs/path-to-regexp). 120 | 121 | Query strings will not be considered when matching requests. 122 | 123 | #### Named routes 124 | 125 | Routes can optionally have names. This allows generation of URLs and easy 126 | renaming of URLs during development. 127 | 128 | ```javascript 129 | router.get('user', '/users/:id', (ctx, next) => { 130 | // ... 131 | }); 132 | 133 | router.url('user', 3); 134 | // => "/users/3" 135 | ``` 136 | 137 | #### Multiple middleware 138 | 139 | Multiple middleware may be given: 140 | 141 | ```javascript 142 | router.get( 143 | '/users/:id', 144 | (ctx, next) => { 145 | return User.findOne(ctx.params.id).then(function(user) { 146 | ctx.user = user; 147 | next(); 148 | }); 149 | }, 150 | ctx => { 151 | console.log(ctx.user); 152 | // => { id: 17, name: "Alex" } 153 | } 154 | ); 155 | ``` 156 | 157 | ### Nested routers 158 | 159 | Nesting routers is supported: 160 | 161 | ```javascript 162 | var forums = new Router(); 163 | var posts = new Router(); 164 | 165 | posts.get('/', (ctx, next) => {...}); 166 | posts.get('/:pid', (ctx, next) => {...}); 167 | forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods()); 168 | 169 | // responds to "/forums/123/posts" and "/forums/123/posts/123" 170 | app.use(forums.routes()); 171 | ``` 172 | 173 | #### Router prefixes 174 | 175 | Route paths can be prefixed at the router level: 176 | 177 | ```javascript 178 | var router = new Router({ 179 | prefix: '/users' 180 | }); 181 | 182 | router.get('/', ...); // responds to "/users" 183 | router.get('/:id', ...); // responds to "/users/:id" 184 | ``` 185 | 186 | #### URL parameters 187 | 188 | Named route parameters are captured and added to `ctx.params`. 189 | 190 | ```javascript 191 | router.get('/:category/:title', (ctx, next) => { 192 | console.log(ctx.params); 193 | // => { category: 'programming', title: 'how-to-node' } 194 | }); 195 | ``` 196 | 197 | The [path-to-regexp](https://github.com/pillarjs/path-to-regexp) module is 198 | used to convert paths to regular expressions. 199 | 200 | **Kind**: instance property of [Router](#exp_module_koa-router--Router) 201 | 202 | | Param | Type | Description | 203 | | --- | --- | --- | 204 | | path | String | | 205 | | [middleware] | function | route middleware(s) | 206 | | callback | function | route callback | 207 | 208 | 209 | 210 | #### router.routes ⇒ function 211 | Returns router middleware which dispatches a route matching the request. 212 | 213 | **Kind**: instance property of [Router](#exp_module_koa-router--Router) 214 | 215 | 216 | #### router.use([path], middleware) ⇒ Router 217 | Use given middleware. 218 | 219 | Middleware run in the order they are defined by `.use()`. They are invoked 220 | sequentially, requests start at the first middleware and work their way 221 | "down" the middleware stack. 222 | 223 | **Kind**: instance method of [Router](#exp_module_koa-router--Router) 224 | 225 | | Param | Type | 226 | | --- | --- | 227 | | [path] | String | 228 | | middleware | function | 229 | | [...] | function | 230 | 231 | **Example** 232 | ```javascript 233 | // session middleware will run before authorize 234 | router 235 | .use(session()) 236 | .use(authorize()); 237 | 238 | // use middleware only with given path 239 | router.use('/users', userAuth()); 240 | 241 | // or with an array of paths 242 | router.use(['/users', '/admin'], userAuth()); 243 | 244 | app.use(router.routes()); 245 | ``` 246 | 247 | 248 | #### router.prefix(prefix) ⇒ Router 249 | Set the path prefix for a Router instance that was already initialized. 250 | 251 | **Kind**: instance method of [Router](#exp_module_koa-router--Router) 252 | 253 | | Param | Type | 254 | | --- | --- | 255 | | prefix | String | 256 | 257 | **Example** 258 | ```javascript 259 | router.prefix('/things/:thing_id') 260 | ``` 261 | 262 | 263 | #### router.allowedMethods([options]) ⇒ function 264 | Returns separate middleware for responding to `OPTIONS` requests with 265 | an `Allow` header containing the allowed methods, as well as responding 266 | with `405 Method Not Allowed` and `501 Not Implemented` as appropriate. 267 | 268 | **Kind**: instance method of [Router](#exp_module_koa-router--Router) 269 | 270 | | Param | Type | Description | 271 | | --- | --- | --- | 272 | | [options] | Object | | 273 | | [options.throw] | Boolean | throw error instead of setting status and header | 274 | | [options.notImplemented] | function | throw the returned value in place of the default NotImplemented error | 275 | | [options.methodNotAllowed] | function | throw the returned value in place of the default MethodNotAllowed error | 276 | 277 | **Example** 278 | ```javascript 279 | var Koa = require('koa'); 280 | var Router = require('koa-router'); 281 | 282 | var app = new Koa(); 283 | var router = new Router(); 284 | 285 | app.use(router.routes()); 286 | app.use(router.allowedMethods()); 287 | ``` 288 | 289 | **Example with [Boom](https://github.com/hapijs/boom)** 290 | 291 | ```javascript 292 | var Koa = require('koa'); 293 | var Router = require('koa-router'); 294 | var Boom = require('boom'); 295 | 296 | var app = new Koa(); 297 | var router = new Router(); 298 | 299 | app.use(router.routes()); 300 | app.use(router.allowedMethods({ 301 | throw: true, 302 | notImplemented: () => new Boom.notImplemented(), 303 | methodNotAllowed: () => new Boom.methodNotAllowed() 304 | })); 305 | ``` 306 | 307 | 308 | #### router.redirect(source, destination, [code]) ⇒ Router 309 | Redirect `source` to `destination` URL with optional 30x status `code`. 310 | 311 | Both `source` and `destination` can be route names. 312 | 313 | ```javascript 314 | router.redirect('/login', 'sign-in'); 315 | ``` 316 | 317 | This is equivalent to: 318 | 319 | ```javascript 320 | router.all('/login', ctx => { 321 | ctx.redirect('/sign-in'); 322 | ctx.status = 301; 323 | }); 324 | ``` 325 | 326 | **Kind**: instance method of [Router](#exp_module_koa-router--Router) 327 | 328 | | Param | Type | Description | 329 | | --- | --- | --- | 330 | | source | String | URL or route name. | 331 | | destination | String | URL or route name. | 332 | | [code] | Number | HTTP status code (default: 301). | 333 | 334 | 335 | 336 | #### router.route(name) ⇒ Layer | false 337 | Lookup route with given `name`. 338 | 339 | **Kind**: instance method of [Router](#exp_module_koa-router--Router) 340 | 341 | | Param | Type | 342 | | --- | --- | 343 | | name | String | 344 | 345 | 346 | 347 | #### router.url(name, params, [options]) ⇒ String | Error 348 | Generate URL for route. Takes a route name and map of named `params`. 349 | 350 | **Kind**: instance method of [Router](#exp_module_koa-router--Router) 351 | 352 | | Param | Type | Description | 353 | | --- | --- | --- | 354 | | name | String | route name | 355 | | params | Object | url parameters | 356 | | [options] | Object | options parameter | 357 | | [options.query] | Object | String | query options | 358 | 359 | **Example** 360 | ```javascript 361 | router.get('user', '/users/:id', (ctx, next) => { 362 | // ... 363 | }); 364 | 365 | router.url('user', 3); 366 | // => "/users/3" 367 | 368 | router.url('user', { id: 3 }); 369 | // => "/users/3" 370 | 371 | router.use((ctx, next) => { 372 | // redirect to named route 373 | ctx.redirect(ctx.router.url('sign-in')); 374 | }) 375 | 376 | router.url('user', { id: 3 }, { query: { limit: 1 } }); 377 | // => "/users/3?limit=1" 378 | 379 | router.url('user', { id: 3 }, { query: "limit=1" }); 380 | // => "/users/3?limit=1" 381 | ``` 382 | 383 | 384 | #### router.param(param, middleware) ⇒ Router 385 | Run middleware for named route parameters. Useful for auto-loading or 386 | validation. 387 | 388 | **Kind**: instance method of [Router](#exp_module_koa-router--Router) 389 | 390 | | Param | Type | 391 | | --- | --- | 392 | | param | String | 393 | | middleware | function | 394 | 395 | **Example** 396 | ```javascript 397 | router 398 | .param('user', (id, ctx, next) => { 399 | ctx.user = users[id]; 400 | if (!ctx.user) return ctx.status = 404; 401 | return next(); 402 | }) 403 | .get('/users/:user', ctx => { 404 | ctx.body = ctx.user; 405 | }) 406 | .get('/users/:user/friends', ctx => { 407 | return ctx.user.getFriends().then(function(friends) { 408 | ctx.body = friends; 409 | }); 410 | }) 411 | // /users/3 => {"id": 3, "name": "Alex"} 412 | // /users/3/friends => [{"id": 4, "name": "TJ"}] 413 | ``` 414 | 415 | 416 | #### Router.url(path, params [, options]) ⇒ String 417 | Generate URL from url pattern and given `params`. 418 | 419 | **Kind**: static method of [Router](#exp_module_koa-router--Router) 420 | 421 | | Param | Type | Description | 422 | | --- | --- | --- | 423 | | path | String | url pattern | 424 | | params | Object | url parameters | 425 | | [options] | Object | options parameter | 426 | | [options.query] | Object | String | query options | 427 | 428 | **Example** 429 | ```javascript 430 | var url = Router.url('/users/:id', {id: 1}); 431 | // => "/users/1" 432 | 433 | const url = Router.url('/users/:id', {id: 1}, {query: { active: true }}); 434 | // => "/users/1?active=true" 435 | ``` 436 | ## Contributing 437 | 438 | Please submit all issues and pull requests to the [alexmingoia/koa-router](http://github.com/alexmingoia/koa-router) repository! 439 | 440 | ## Tests 441 | 442 | Run tests using `npm test`. 443 | 444 | ## Support 445 | 446 | If you have any problem or suggestion please open an issue [here](https://github.com/alexmingoia/koa-router/issues). 447 | -------------------------------------------------------------------------------- /bench/Makefile: -------------------------------------------------------------------------------- 1 | all: middleware 2 | 3 | middleware: 4 | @./run 1 false 5 | @./run 5 false 6 | @./run 10 false 7 | @./run 20 false 8 | @./run 50 false 9 | @./run 100 false 10 | @./run 200 false 11 | @./run 500 false 12 | @./run 1000 false 13 | @./run 1 true 14 | @./run 5 true 15 | @./run 10 true 16 | @./run 20 true 17 | @./run 50 true 18 | @./run 100 true 19 | @./run 200 true 20 | @./run 500 true 21 | @./run 1000 true 22 | 23 | .PHONY: all middleware 24 | -------------------------------------------------------------------------------- /bench/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | export FACTOR=$1 6 | export USE_MIDDLEWARE=$2 7 | export PORT=3333 8 | 9 | host="http://localhost:$PORT" 10 | 11 | node "$(dirname $0)/server.js" & 12 | 13 | pid=$! 14 | 15 | curl \ 16 | --retry-connrefused \ 17 | --retry 5 \ 18 | --retry-delay 0 \ 19 | -s \ 20 | "$host/_health" \ 21 | > /dev/null 22 | 23 | # siege -c 50 -t 8 "$host/10/child/grandchild/%40" 24 | wrk "$host/10/child/grandchild/%40" \ 25 | -d 3 \ 26 | -c 50 \ 27 | -t 8 \ 28 | | grep 'Requests/sec' \ 29 | | awk '{ print " " $2 }' 30 | 31 | kill $pid 32 | -------------------------------------------------------------------------------- /bench/server.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const Router = require('../'); 3 | 4 | const app = new Koa(); 5 | const router = new Router(); 6 | 7 | const ok = ctx => ctx.status = 200; 8 | const n = parseInt(process.env.FACTOR || '10', 10); 9 | const useMiddleware = process.env.USE_MIDDLEWARE === 'true'; 10 | 11 | router.get('/_health', ok); 12 | 13 | for (let i = n; i > 0; i--) { 14 | if (useMiddleware) router.use((ctx, next) => next()); 15 | router.get(`/${i}/one`, ok); 16 | router.get(`/${i}/one/two`, ok); 17 | router.get(`/${i}/one/two/:three`, ok); 18 | router.get(`/${i}/one/two/:three/:four?`, ok); 19 | router.get(`/${i}/one/two/:three/:four?/five`, ok); 20 | router.get(`/${i}/one/two/:three/:four?/five/six`, ok); 21 | } 22 | 23 | const grandchild = new Router(); 24 | 25 | if (useMiddleware) grandchild.use((ctx, next) => next()); 26 | grandchild.get('/', ok); 27 | grandchild.get('/:id', ok); 28 | grandchild.get('/:id/seven', ok); 29 | grandchild.get('/:id/seven(/eight)?', ok); 30 | 31 | for (let i = n; i > 0; i--) { 32 | let child = new Router(); 33 | if (useMiddleware) child.use((ctx, next) => next()); 34 | child.get(`/:${''.padStart(i, 'a')}`, ok); 35 | child.nest('/grandchild', grandchild); 36 | router.nest(`/${i}/child`, child); 37 | } 38 | 39 | if (process.env.DEBUG) { 40 | console.log(require('../lib/utils').inspect(router)); 41 | } 42 | 43 | app.use(router.routes()); 44 | 45 | process.stdout.write(`mw: ${useMiddleware} factor: ${n}`); 46 | 47 | app.listen(process.env.PORT); 48 | -------------------------------------------------------------------------------- /history.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ## 7.4.0 4 | 5 | - Fix router.url() for multiple nested routers [#407](https://github.com/alexmingoia/koa-router/pull/407) 6 | - `layer.name` added to `ctx` at `ctx.routerName` during routing [#412](https://github.com/alexmingoia/koa-router/pull/412) 7 | - Router.use() was erroneously settings `(.*)` as a prefix to all routers nested with .use that did not pass an explicit prefix string as the first argument. This resulted in routes being matched that should not have been, included the running of multiple route handlers in error. [#369](https://github.com/alexmingoia/koa-router/issues/369) and [#410](https://github.com/alexmingoia/koa-router/issues/410) include information on this issue. 8 | 9 | ## 7.3.0 10 | 11 | - Router#url() now accepts query parameters to add to generated urls [#396](https://github.com/alexmingoia/koa-router/pull/396) 12 | 13 | ## 7.2.1 14 | 15 | - Respond to CORS preflights with 200, 0 length body [#359](https://github.com/alexmingoia/koa-router/issues/359) 16 | 17 | ## 7.2.0 18 | 19 | - Fix a bug in Router#url and append Router object to ctx. [#350](https://github.com/alexmingoia/koa-router/pull/350) 20 | - Adds `_matchedRouteName` to context [#337](https://github.com/alexmingoia/koa-router/pull/337) 21 | - Respond to CORS preflights with 200, 0 length body [#359](https://github.com/alexmingoia/koa-router/issues/359) 22 | 23 | ## 7.1.1 24 | 25 | - Fix bug where param handlers were run out of order [#282](https://github.com/alexmingoia/koa-router/pull/282) 26 | 27 | ## 7.1.0 28 | 29 | - Backports: merge 5.4 work into the 7.x upstream. See 5.4.0 updates for more details. 30 | 31 | ## 7.0.1 32 | 33 | - Fix: allowedMethods should be ctx.method not this.method [#215](https://github.com/alexmingoia/koa-router/pull/215) 34 | 35 | ## 7.0.0 36 | 37 | - The API has changed to match the new promise-based middleware 38 | signature of koa 2. See the 39 | [koa 2.x readme](https://github.com/koajs/koa/tree/2.0.0-alpha.3) for more 40 | information. 41 | - Middleware is now always run in the order declared by `.use()` (or `.get()`, 42 | etc.), which matches Express 4 API. 43 | 44 | ## 5.4.0 45 | 46 | - Expose matched route at `ctx._matchedRoute`. 47 | 48 | ## 5.3.0 49 | 50 | - Register multiple routes with array of paths [#203](https://github.com/alexmingoia/koa-router/issue/143). 51 | - Improved router.url() [#143](https://github.com/alexmingoia/koa-router/pull/143) 52 | - Adds support for named routes and regular expressions 53 | [#152](https://github.com/alexmingoia/koa-router/pull/152) 54 | - Add support for custom throw functions for 405 and 501 responses [#206](https://github.com/alexmingoia/koa-router/pull/206) 55 | 56 | ## 5.2.3 57 | 58 | - Fix for middleware running twice when nesting routes [#184](https://github.com/alexmingoia/koa-router/issues/184) 59 | 60 | ## 5.2.2 61 | 62 | - Register routes without params before those with params [#183](https://github.com/alexmingoia/koa-router/pull/183) 63 | - Fix for allowed methods [#182](https://github.com/alexmingoia/koa-router/issues/182) 64 | 65 | ## 5.2.0 66 | 67 | - Add support for async/await. Resolves [#130](https://github.com/alexmingoia/koa-router/issues/130). 68 | - Add support for array of paths by Router#use(). Resolves [#175](https://github.com/alexmingoia/koa-router/issues/175). 69 | - Inherit param middleware when nesting routers. Fixes [#170](https://github.com/alexmingoia/koa-router/issues/170). 70 | - Default router middleware without path to root. Fixes [#161](https://github.com/alexmingoia/koa-router/issues/161), [#155](https://github.com/alexmingoia/koa-router/issues/155), [#156](https://github.com/alexmingoia/koa-router/issues/156). 71 | - Run nested router middleware after parent's. Fixes [#156](https://github.com/alexmingoia/koa-router/issues/156). 72 | - Remove dependency on koa-compose. 73 | 74 | ## 5.1.1 75 | 76 | - Match routes in order they were defined. Fixes #131. 77 | 78 | ## 5.1.0 79 | 80 | - Support mounting router middleware at a given path. 81 | 82 | ## 5.0.1 83 | 84 | - Fix bug with missing parameters when nesting routers. 85 | 86 | ## 5.0.0 87 | 88 | - Remove confusing API for extending koa app with router methods. Router#use() 89 | does not have the same behavior as app#use(). 90 | - Add support for nesting routes. 91 | - Remove support for regular expression routes to achieve nestable routers and 92 | enable future trie-based routing optimizations. 93 | 94 | ## 4.3.2 95 | 96 | - Do not send 405 if route matched but status is 404. Fixes #112, closes #114. 97 | 98 | ## 4.3.1 99 | 100 | - Do not run middleware if not yielded to by previous middleware. Fixes #115. 101 | 102 | ## 4.3.0 103 | 104 | - Add support for router prefixes. 105 | - Add MIT license. 106 | 107 | ## 4.2.0 108 | 109 | - Fixed issue with router middleware being applied even if no route was 110 | matched. 111 | - Router.url - new static method to generate url from url pattern and data 112 | 113 | ## 4.1.0 114 | 115 | Private API changed to separate context parameter decoration from route 116 | matching. `Router#match` and `Route#match` are now pure functions that return 117 | an array of routes that match the URL path. 118 | 119 | For modules using this private API that need to determine if a method and path 120 | match a route, `route.methods` must be checked against the routes returned from 121 | `router.match()`: 122 | 123 | ```javascript 124 | var matchedRoute = router.match(path).filter(function (route) { 125 | return ~route.methods.indexOf(method); 126 | }).shift(); 127 | ``` 128 | 129 | ## 4.0.0 130 | 131 | 405, 501, and OPTIONS response handling was moved into separate middleware 132 | `router.allowedMethods()`. This resolves incorrect 501 or 405 responses when 133 | using multiple routers. 134 | 135 | ### Breaking changes 136 | 137 | 4.x is mostly backwards compatible with 3.x, except for the following: 138 | 139 | - Instantiating a router with `new` and `app` returns the router instance, 140 | whereas 3.x returns the router middleware. When creating a router in 4.x, the 141 | only time router middleware is returned is when creating using the 142 | `Router(app)` signature (with `app` and without `new`). 143 | -------------------------------------------------------------------------------- /lib/README_tpl.hbs: -------------------------------------------------------------------------------- 1 | # koa-router 2 | 3 | [![NPM version](https://img.shields.io/npm/v/koa-router.svg?style=flat)](https://npmjs.org/package/koa-router) [![NPM Downloads](https://img.shields.io/npm/dm/koa-router.svg?style=flat)](https://npmjs.org/package/koa-router) [![Node.js Version](https://img.shields.io/node/v/koa-router.svg?style=flat)](http://nodejs.org/download/) [![Build Status](https://img.shields.io/travis/alexmingoia/koa-router.svg?style=flat)](http://travis-ci.org/alexmingoia/koa-router) [![Tips](https://img.shields.io/gratipay/alexmingoia.svg?style=flat)](https://www.gratipay.com/alexmingoia/) [![Gitter Chat](https://img.shields.io/badge/gitter-join%20chat-1dce73.svg?style=flat)](https://gitter.im/alexmingoia/koa-router/) 4 | 5 | > Router middleware for [koa](https://github.com/koajs/koa) 6 | 7 | * Express-style routing using `app.get`, `app.put`, `app.post`, etc. 8 | * Named URL parameters. 9 | * Named routes with URL generation. 10 | * Responds to `OPTIONS` requests with allowed methods. 11 | * Support for `405 Method Not Allowed` and `501 Not Implemented`. 12 | * Multiple route middleware. 13 | * Multiple routers. 14 | * Nestable routers. 15 | * ES7 async/await support. 16 | 17 | {{#module name="koa-router"}}{{>body}}{{/module}}## Migrating to 7 / Koa 2 18 | 19 | - The API has changed to match the new promise-based middleware 20 | signature of koa 2. See the 21 | [koa 2.x readme](https://github.com/koajs/koa/tree/2.0.0-alpha.3) for more 22 | information. 23 | - Middleware is now always run in the order declared by `.use()` (or `.get()`, 24 | etc.), which matches Express 4 API. 25 | 26 | ## Installation 27 | 28 | Install using [npm](https://www.npmjs.org/): 29 | 30 | ```sh 31 | npm install koa-router 32 | ``` 33 | 34 | ## API Reference 35 | {{#module name="koa-router"~}} 36 | {{>body~}} 37 | {{>member-index~}} 38 | {{>members~}} 39 | {{/module~}} 40 | 41 | ## Contributing 42 | 43 | Please submit all issues and pull requests to the [alexmingoia/koa-router](http://github.com/alexmingoia/koa-router) repository! 44 | 45 | ## Tests 46 | 47 | Run tests using `npm test`. 48 | 49 | ## Support 50 | 51 | If you have any problem or suggestion please open an issue [here](https://github.com/alexmingoia/koa-router/issues). 52 | -------------------------------------------------------------------------------- /lib/layer.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('koa-router'); 2 | var pathToRegExp = require('path-to-regexp'); 3 | var uri = require('urijs'); 4 | 5 | module.exports = Layer; 6 | 7 | /** 8 | * Initialize a new routing Layer with given `method`, `path`, and `middleware`. 9 | * 10 | * @param {String|RegExp} path Path string or regular expression. 11 | * @param {Array} methods Array of HTTP verbs. 12 | * @param {Array} middleware Layer callback/middleware or series of. 13 | * @param {Object=} opts 14 | * @param {String=} opts.name route name 15 | * @param {String=} opts.sensitive case sensitive (default: false) 16 | * @param {String=} opts.strict require the trailing slash (default: false) 17 | * @returns {Layer} 18 | * @private 19 | */ 20 | 21 | function Layer(path, methods, middleware, opts) { 22 | this.opts = opts || {}; 23 | this.name = this.opts.name || null; 24 | this.methods = []; 25 | this.paramNames = []; 26 | this.stack = Array.isArray(middleware) ? middleware : [middleware]; 27 | 28 | methods.forEach(function(method) { 29 | var l = this.methods.push(method.toUpperCase()); 30 | if (this.methods[l-1] === 'GET') { 31 | this.methods.unshift('HEAD'); 32 | } 33 | }, this); 34 | 35 | // ensure middleware is a function 36 | this.stack.forEach(function(fn) { 37 | var type = (typeof fn); 38 | if (type !== 'function') { 39 | throw new Error( 40 | methods.toString() + " `" + (this.opts.name || path) +"`: `middleware` " 41 | + "must be a function, not `" + type + "`" 42 | ); 43 | } 44 | }, this); 45 | 46 | this.path = path; 47 | this.regexp = pathToRegExp(path, this.paramNames, this.opts); 48 | 49 | debug('defined route %s %s', this.methods, this.opts.prefix + this.path); 50 | }; 51 | 52 | /** 53 | * Returns whether request `path` matches route. 54 | * 55 | * @param {String} path 56 | * @returns {Boolean} 57 | * @private 58 | */ 59 | 60 | Layer.prototype.match = function (path) { 61 | return this.regexp.test(path); 62 | }; 63 | 64 | /** 65 | * Returns map of URL parameters for given `path` and `paramNames`. 66 | * 67 | * @param {String} path 68 | * @param {Array.} captures 69 | * @param {Object=} existingParams 70 | * @returns {Object} 71 | * @private 72 | */ 73 | 74 | Layer.prototype.params = function (path, captures, existingParams) { 75 | var params = existingParams || {}; 76 | 77 | for (var len = captures.length, i=0; i} 92 | * @private 93 | */ 94 | 95 | Layer.prototype.captures = function (path) { 96 | if (this.opts.ignoreCaptures) return []; 97 | return path.match(this.regexp).slice(1); 98 | }; 99 | 100 | /** 101 | * Generate URL for route using given `params`. 102 | * 103 | * @example 104 | * 105 | * ```javascript 106 | * var route = new Layer(['GET'], '/users/:id', fn); 107 | * 108 | * route.url({ id: 123 }); // => "/users/123" 109 | * ``` 110 | * 111 | * @param {Object} params url parameters 112 | * @returns {String} 113 | * @private 114 | */ 115 | 116 | Layer.prototype.url = function (params, options) { 117 | var args = params; 118 | var url = this.path.replace(/\(\.\*\)/g, ''); 119 | var toPath = pathToRegExp.compile(url); 120 | var replaced; 121 | 122 | if (typeof params != 'object') { 123 | args = Array.prototype.slice.call(arguments); 124 | if (typeof args[args.length - 1] == 'object') { 125 | options = args[args.length - 1]; 126 | args = args.slice(0, args.length - 1); 127 | } 128 | } 129 | 130 | var tokens = pathToRegExp.parse(url); 131 | var replace = {}; 132 | 133 | if (args instanceof Array) { 134 | for (var len = tokens.length, i=0, j=0; i token.name)) { 138 | replace = params; 139 | } else { 140 | options = params; 141 | } 142 | 143 | replaced = toPath(replace); 144 | 145 | if (options && options.query) { 146 | var replaced = new uri(replaced) 147 | replaced.search(options.query); 148 | return replaced.toString(); 149 | } 150 | 151 | return replaced; 152 | }; 153 | 154 | /** 155 | * Run validations on route named parameters. 156 | * 157 | * @example 158 | * 159 | * ```javascript 160 | * router 161 | * .param('user', function (id, ctx, next) { 162 | * ctx.user = users[id]; 163 | * if (!user) return ctx.status = 404; 164 | * next(); 165 | * }) 166 | * .get('/users/:user', function (ctx, next) { 167 | * ctx.body = ctx.user; 168 | * }); 169 | * ``` 170 | * 171 | * @param {String} param 172 | * @param {Function} middleware 173 | * @returns {Layer} 174 | * @private 175 | */ 176 | 177 | Layer.prototype.param = function (param, fn) { 178 | var stack = this.stack; 179 | var params = this.paramNames; 180 | var middleware = function (ctx, next) { 181 | return fn.call(this, ctx.params[param], ctx, next); 182 | }; 183 | middleware.param = param; 184 | 185 | var names = params.map(function (p) { 186 | return p.name; 187 | }); 188 | 189 | var x = names.indexOf(param); 190 | if (x > -1) { 191 | // iterate through the stack, to figure out where to place the handler fn 192 | stack.some(function (fn, i) { 193 | // param handlers are always first, so when we find an fn w/o a param property, stop here 194 | // if the param handler at this part of the stack comes after the one we are adding, stop here 195 | if (!fn.param || names.indexOf(fn.param) > x) { 196 | // inject this param handler right before the current item 197 | stack.splice(i, 0, middleware); 198 | return true; // then break the loop 199 | } 200 | }); 201 | } 202 | 203 | return this; 204 | }; 205 | 206 | /** 207 | * Prefix route path. 208 | * 209 | * @param {String} prefix 210 | * @returns {Layer} 211 | * @private 212 | */ 213 | 214 | Layer.prototype.setPrefix = function (prefix) { 215 | if (this.path) { 216 | this.path = prefix + this.path; 217 | this.paramNames = []; 218 | this.regexp = pathToRegExp(this.path, this.paramNames, this.opts); 219 | } 220 | 221 | return this; 222 | }; 223 | 224 | /** 225 | * Safe decodeURIComponent, won't throw any error. 226 | * If `decodeURIComponent` error happen, just return the original value. 227 | * 228 | * @param {String} text 229 | * @returns {String} URL decode original string. 230 | * @private 231 | */ 232 | 233 | function safeDecodeURIComponent(text) { 234 | try { 235 | return decodeURIComponent(text); 236 | } catch (e) { 237 | return text; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * RESTful resource routing middleware for koa. 3 | * 4 | * @author Alex Mingoia 5 | * @link https://github.com/alexmingoia/koa-router 6 | */ 7 | 8 | var debug = require('debug')('koa-router'); 9 | var compose = require('koa-compose'); 10 | var HttpError = require('http-errors'); 11 | var methods = require('methods'); 12 | var Layer = require('./layer'); 13 | 14 | /** 15 | * @module koa-router 16 | */ 17 | 18 | module.exports = Router; 19 | 20 | /** 21 | * Create a new router. 22 | * 23 | * @example 24 | * 25 | * Basic usage: 26 | * 27 | * ```javascript 28 | * var Koa = require('koa'); 29 | * var Router = require('koa-router'); 30 | * 31 | * var app = new Koa(); 32 | * var router = new Router(); 33 | * 34 | * router.get('/', (ctx, next) => { 35 | * // ctx.router available 36 | * }); 37 | * 38 | * app 39 | * .use(router.routes()) 40 | * .use(router.allowedMethods()); 41 | * ``` 42 | * 43 | * @alias module:koa-router 44 | * @param {Object=} opts 45 | * @param {String=} opts.prefix prefix router paths 46 | * @constructor 47 | */ 48 | 49 | function Router(opts) { 50 | if (!(this instanceof Router)) { 51 | return new Router(opts); 52 | } 53 | 54 | this.opts = opts || {}; 55 | this.methods = this.opts.methods || [ 56 | 'HEAD', 57 | 'OPTIONS', 58 | 'GET', 59 | 'PUT', 60 | 'PATCH', 61 | 'POST', 62 | 'DELETE' 63 | ]; 64 | 65 | this.params = {}; 66 | this.stack = []; 67 | }; 68 | 69 | /** 70 | * Create `router.verb()` methods, where *verb* is one of the HTTP verbs such 71 | * as `router.get()` or `router.post()`. 72 | * 73 | * Match URL patterns to callback functions or controller actions using `router.verb()`, 74 | * where **verb** is one of the HTTP verbs such as `router.get()` or `router.post()`. 75 | * 76 | * Additionaly, `router.all()` can be used to match against all methods. 77 | * 78 | * ```javascript 79 | * router 80 | * .get('/', (ctx, next) => { 81 | * ctx.body = 'Hello World!'; 82 | * }) 83 | * .post('/users', (ctx, next) => { 84 | * // ... 85 | * }) 86 | * .put('/users/:id', (ctx, next) => { 87 | * // ... 88 | * }) 89 | * .del('/users/:id', (ctx, next) => { 90 | * // ... 91 | * }) 92 | * .all('/users/:id', (ctx, next) => { 93 | * // ... 94 | * }); 95 | * ``` 96 | * 97 | * When a route is matched, its path is available at `ctx._matchedRoute` and if named, 98 | * the name is available at `ctx._matchedRouteName` 99 | * 100 | * Route paths will be translated to regular expressions using 101 | * [path-to-regexp](https://github.com/pillarjs/path-to-regexp). 102 | * 103 | * Query strings will not be considered when matching requests. 104 | * 105 | * #### Named routes 106 | * 107 | * Routes can optionally have names. This allows generation of URLs and easy 108 | * renaming of URLs during development. 109 | * 110 | * ```javascript 111 | * router.get('user', '/users/:id', (ctx, next) => { 112 | * // ... 113 | * }); 114 | * 115 | * router.url('user', 3); 116 | * // => "/users/3" 117 | * ``` 118 | * 119 | * #### Multiple middleware 120 | * 121 | * Multiple middleware may be given: 122 | * 123 | * ```javascript 124 | * router.get( 125 | * '/users/:id', 126 | * (ctx, next) => { 127 | * return User.findOne(ctx.params.id).then(function(user) { 128 | * ctx.user = user; 129 | * next(); 130 | * }); 131 | * }, 132 | * ctx => { 133 | * console.log(ctx.user); 134 | * // => { id: 17, name: "Alex" } 135 | * } 136 | * ); 137 | * ``` 138 | * 139 | * ### Nested routers 140 | * 141 | * Nesting routers is supported: 142 | * 143 | * ```javascript 144 | * var forums = new Router(); 145 | * var posts = new Router(); 146 | * 147 | * posts.get('/', (ctx, next) => {...}); 148 | * posts.get('/:pid', (ctx, next) => {...}); 149 | * forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods()); 150 | * 151 | * // responds to "/forums/123/posts" and "/forums/123/posts/123" 152 | * app.use(forums.routes()); 153 | * ``` 154 | * 155 | * #### Router prefixes 156 | * 157 | * Route paths can be prefixed at the router level: 158 | * 159 | * ```javascript 160 | * var router = new Router({ 161 | * prefix: '/users' 162 | * }); 163 | * 164 | * router.get('/', ...); // responds to "/users" 165 | * router.get('/:id', ...); // responds to "/users/:id" 166 | * ``` 167 | * 168 | * #### URL parameters 169 | * 170 | * Named route parameters are captured and added to `ctx.params`. 171 | * 172 | * ```javascript 173 | * router.get('/:category/:title', (ctx, next) => { 174 | * console.log(ctx.params); 175 | * // => { category: 'programming', title: 'how-to-node' } 176 | * }); 177 | * ``` 178 | * 179 | * The [path-to-regexp](https://github.com/pillarjs/path-to-regexp) module is 180 | * used to convert paths to regular expressions. 181 | * 182 | * @name get|put|post|patch|delete|del 183 | * @memberof module:koa-router.prototype 184 | * @param {String} path 185 | * @param {Function=} middleware route middleware(s) 186 | * @param {Function} callback route callback 187 | * @returns {Router} 188 | */ 189 | 190 | methods.forEach(function (method) { 191 | Router.prototype[method] = function (name, path, middleware) { 192 | var middleware; 193 | 194 | if (typeof path === 'string' || path instanceof RegExp) { 195 | middleware = Array.prototype.slice.call(arguments, 2); 196 | } else { 197 | middleware = Array.prototype.slice.call(arguments, 1); 198 | path = name; 199 | name = null; 200 | } 201 | 202 | this.register(path, [method], middleware, { 203 | name: name 204 | }); 205 | 206 | return this; 207 | }; 208 | }); 209 | 210 | // Alias for `router.delete()` because delete is a reserved word 211 | Router.prototype.del = Router.prototype['delete']; 212 | 213 | /** 214 | * Use given middleware. 215 | * 216 | * Middleware run in the order they are defined by `.use()`. They are invoked 217 | * sequentially, requests start at the first middleware and work their way 218 | * "down" the middleware stack. 219 | * 220 | * @example 221 | * 222 | * ```javascript 223 | * // session middleware will run before authorize 224 | * router 225 | * .use(session()) 226 | * .use(authorize()); 227 | * 228 | * // use middleware only with given path 229 | * router.use('/users', userAuth()); 230 | * 231 | * // or with an array of paths 232 | * router.use(['/users', '/admin'], userAuth()); 233 | * 234 | * app.use(router.routes()); 235 | * ``` 236 | * 237 | * @param {String=} path 238 | * @param {Function} middleware 239 | * @param {Function=} ... 240 | * @returns {Router} 241 | */ 242 | 243 | Router.prototype.use = function () { 244 | var router = this; 245 | var middleware = Array.prototype.slice.call(arguments); 246 | var path; 247 | 248 | // support array of paths 249 | if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') { 250 | middleware[0].forEach(function (p) { 251 | router.use.apply(router, [p].concat(middleware.slice(1))); 252 | }); 253 | 254 | return this; 255 | } 256 | 257 | var hasPath = typeof middleware[0] === 'string'; 258 | if (hasPath) { 259 | path = middleware.shift(); 260 | } 261 | 262 | middleware.forEach(function (m) { 263 | if (m.router) { 264 | m.router.stack.forEach(function (nestedLayer) { 265 | if (path) nestedLayer.setPrefix(path); 266 | if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix); 267 | router.stack.push(nestedLayer); 268 | }); 269 | 270 | if (router.params) { 271 | Object.keys(router.params).forEach(function (key) { 272 | m.router.param(key, router.params[key]); 273 | }); 274 | } 275 | } else { 276 | router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath }); 277 | } 278 | }); 279 | 280 | return this; 281 | }; 282 | 283 | /** 284 | * Set the path prefix for a Router instance that was already initialized. 285 | * 286 | * @example 287 | * 288 | * ```javascript 289 | * router.prefix('/things/:thing_id') 290 | * ``` 291 | * 292 | * @param {String} prefix 293 | * @returns {Router} 294 | */ 295 | 296 | Router.prototype.prefix = function (prefix) { 297 | prefix = prefix.replace(/\/$/, ''); 298 | 299 | this.opts.prefix = prefix; 300 | 301 | this.stack.forEach(function (route) { 302 | route.setPrefix(prefix); 303 | }); 304 | 305 | return this; 306 | }; 307 | 308 | /** 309 | * Returns router middleware which dispatches a route matching the request. 310 | * 311 | * @returns {Function} 312 | */ 313 | 314 | Router.prototype.routes = Router.prototype.middleware = function () { 315 | var router = this; 316 | 317 | var dispatch = function dispatch(ctx, next) { 318 | debug('%s %s', ctx.method, ctx.path); 319 | 320 | var path = router.opts.routerPath || ctx.routerPath || ctx.path; 321 | var matched = router.match(path, ctx.method); 322 | var layerChain, layer, i; 323 | 324 | if (ctx.matched) { 325 | ctx.matched.push.apply(ctx.matched, matched.path); 326 | } else { 327 | ctx.matched = matched.path; 328 | } 329 | 330 | ctx.router = router; 331 | 332 | if (!matched.route) return next(); 333 | 334 | var matchedLayers = matched.pathAndMethod 335 | var mostSpecificLayer = matchedLayers[matchedLayers.length - 1] 336 | ctx._matchedRoute = mostSpecificLayer.path; 337 | if (mostSpecificLayer.name) { 338 | ctx._matchedRouteName = mostSpecificLayer.name; 339 | } 340 | 341 | layerChain = matchedLayers.reduce(function(memo, layer) { 342 | memo.push(function(ctx, next) { 343 | ctx.captures = layer.captures(path, ctx.captures); 344 | ctx.params = layer.params(path, ctx.captures, ctx.params); 345 | ctx.routerName = layer.name; 346 | return next(); 347 | }); 348 | return memo.concat(layer.stack); 349 | }, []); 350 | 351 | return compose(layerChain)(ctx, next); 352 | }; 353 | 354 | dispatch.router = this; 355 | 356 | return dispatch; 357 | }; 358 | 359 | /** 360 | * Returns separate middleware for responding to `OPTIONS` requests with 361 | * an `Allow` header containing the allowed methods, as well as responding 362 | * with `405 Method Not Allowed` and `501 Not Implemented` as appropriate. 363 | * 364 | * @example 365 | * 366 | * ```javascript 367 | * var Koa = require('koa'); 368 | * var Router = require('koa-router'); 369 | * 370 | * var app = new Koa(); 371 | * var router = new Router(); 372 | * 373 | * app.use(router.routes()); 374 | * app.use(router.allowedMethods()); 375 | * ``` 376 | * 377 | * **Example with [Boom](https://github.com/hapijs/boom)** 378 | * 379 | * ```javascript 380 | * var Koa = require('koa'); 381 | * var Router = require('koa-router'); 382 | * var Boom = require('boom'); 383 | * 384 | * var app = new Koa(); 385 | * var router = new Router(); 386 | * 387 | * app.use(router.routes()); 388 | * app.use(router.allowedMethods({ 389 | * throw: true, 390 | * notImplemented: () => new Boom.notImplemented(), 391 | * methodNotAllowed: () => new Boom.methodNotAllowed() 392 | * })); 393 | * ``` 394 | * 395 | * @param {Object=} options 396 | * @param {Boolean=} options.throw throw error instead of setting status and header 397 | * @param {Function=} options.notImplemented throw the returned value in place of the default NotImplemented error 398 | * @param {Function=} options.methodNotAllowed throw the returned value in place of the default MethodNotAllowed error 399 | * @returns {Function} 400 | */ 401 | 402 | Router.prototype.allowedMethods = function (options) { 403 | options = options || {}; 404 | var implemented = this.methods; 405 | 406 | return function allowedMethods(ctx, next) { 407 | return next().then(function() { 408 | var allowed = {}; 409 | 410 | if (!ctx.status || ctx.status === 404) { 411 | ctx.matched.forEach(function (route) { 412 | route.methods.forEach(function (method) { 413 | allowed[method] = method; 414 | }); 415 | }); 416 | 417 | var allowedArr = Object.keys(allowed); 418 | 419 | if (!~implemented.indexOf(ctx.method)) { 420 | if (options.throw) { 421 | var notImplementedThrowable; 422 | if (typeof options.notImplemented === 'function') { 423 | notImplementedThrowable = options.notImplemented(); // set whatever the user returns from their function 424 | } else { 425 | notImplementedThrowable = new HttpError.NotImplemented(); 426 | } 427 | throw notImplementedThrowable; 428 | } else { 429 | ctx.status = 501; 430 | ctx.set('Allow', allowedArr.join(', ')); 431 | } 432 | } else if (allowedArr.length) { 433 | if (ctx.method === 'OPTIONS') { 434 | ctx.status = 200; 435 | ctx.body = ''; 436 | ctx.set('Allow', allowedArr.join(', ')); 437 | } else if (!allowed[ctx.method]) { 438 | if (options.throw) { 439 | var notAllowedThrowable; 440 | if (typeof options.methodNotAllowed === 'function') { 441 | notAllowedThrowable = options.methodNotAllowed(); // set whatever the user returns from their function 442 | } else { 443 | notAllowedThrowable = new HttpError.MethodNotAllowed(); 444 | } 445 | throw notAllowedThrowable; 446 | } else { 447 | ctx.status = 405; 448 | ctx.set('Allow', allowedArr.join(', ')); 449 | } 450 | } 451 | } 452 | } 453 | }); 454 | }; 455 | }; 456 | 457 | /** 458 | * Register route with all methods. 459 | * 460 | * @param {String} name Optional. 461 | * @param {String} path 462 | * @param {Function=} middleware You may also pass multiple middleware. 463 | * @param {Function} callback 464 | * @returns {Router} 465 | * @private 466 | */ 467 | 468 | Router.prototype.all = function (name, path, middleware) { 469 | var middleware; 470 | 471 | if (typeof path === 'string') { 472 | middleware = Array.prototype.slice.call(arguments, 2); 473 | } else { 474 | middleware = Array.prototype.slice.call(arguments, 1); 475 | path = name; 476 | name = null; 477 | } 478 | 479 | this.register(path, methods, middleware, { 480 | name: name 481 | }); 482 | 483 | return this; 484 | }; 485 | 486 | /** 487 | * Redirect `source` to `destination` URL with optional 30x status `code`. 488 | * 489 | * Both `source` and `destination` can be route names. 490 | * 491 | * ```javascript 492 | * router.redirect('/login', 'sign-in'); 493 | * ``` 494 | * 495 | * This is equivalent to: 496 | * 497 | * ```javascript 498 | * router.all('/login', ctx => { 499 | * ctx.redirect('/sign-in'); 500 | * ctx.status = 301; 501 | * }); 502 | * ``` 503 | * 504 | * @param {String} source URL or route name. 505 | * @param {String} destination URL or route name. 506 | * @param {Number=} code HTTP status code (default: 301). 507 | * @returns {Router} 508 | */ 509 | 510 | Router.prototype.redirect = function (source, destination, code) { 511 | // lookup source route by name 512 | if (source[0] !== '/') { 513 | source = this.url(source); 514 | } 515 | 516 | // lookup destination route by name 517 | if (destination[0] !== '/') { 518 | destination = this.url(destination); 519 | } 520 | 521 | return this.all(source, ctx => { 522 | ctx.redirect(destination); 523 | ctx.status = code || 301; 524 | }); 525 | }; 526 | 527 | /** 528 | * Create and register a route. 529 | * 530 | * @param {String} path Path string. 531 | * @param {Array.} methods Array of HTTP verbs. 532 | * @param {Function} middleware Multiple middleware also accepted. 533 | * @returns {Layer} 534 | * @private 535 | */ 536 | 537 | Router.prototype.register = function (path, methods, middleware, opts) { 538 | opts = opts || {}; 539 | 540 | var router = this; 541 | var stack = this.stack; 542 | 543 | // support array of paths 544 | if (Array.isArray(path)) { 545 | path.forEach(function (p) { 546 | router.register.call(router, p, methods, middleware, opts); 547 | }); 548 | 549 | return this; 550 | } 551 | 552 | // create route 553 | var route = new Layer(path, methods, middleware, { 554 | end: opts.end === false ? opts.end : true, 555 | name: opts.name, 556 | sensitive: opts.sensitive || this.opts.sensitive || false, 557 | strict: opts.strict || this.opts.strict || false, 558 | prefix: opts.prefix || this.opts.prefix || "", 559 | ignoreCaptures: opts.ignoreCaptures 560 | }); 561 | 562 | if (this.opts.prefix) { 563 | route.setPrefix(this.opts.prefix); 564 | } 565 | 566 | // add parameter middleware 567 | Object.keys(this.params).forEach(function (param) { 568 | route.param(param, this.params[param]); 569 | }, this); 570 | 571 | stack.push(route); 572 | 573 | return route; 574 | }; 575 | 576 | /** 577 | * Lookup route with given `name`. 578 | * 579 | * @param {String} name 580 | * @returns {Layer|false} 581 | */ 582 | 583 | Router.prototype.route = function (name) { 584 | var routes = this.stack; 585 | 586 | for (var len = routes.length, i=0; i { 602 | * // ... 603 | * }); 604 | * 605 | * router.url('user', 3); 606 | * // => "/users/3" 607 | * 608 | * router.url('user', { id: 3 }); 609 | * // => "/users/3" 610 | * 611 | * router.use((ctx, next) => { 612 | * // redirect to named route 613 | * ctx.redirect(ctx.router.url('sign-in')); 614 | * }) 615 | * 616 | * router.url('user', { id: 3 }, { query: { limit: 1 } }); 617 | * // => "/users/3?limit=1" 618 | * 619 | * router.url('user', { id: 3 }, { query: "limit=1" }); 620 | * // => "/users/3?limit=1" 621 | * ``` 622 | * 623 | * @param {String} name route name 624 | * @param {Object} params url parameters 625 | * @param {Object} [options] options parameter 626 | * @param {Object|String} [options.query] query options 627 | * @returns {String|Error} 628 | */ 629 | 630 | Router.prototype.url = function (name, params) { 631 | var route = this.route(name); 632 | 633 | if (route) { 634 | var args = Array.prototype.slice.call(arguments, 1); 635 | return route.url.apply(route, args); 636 | } 637 | 638 | return new Error("No route found for name: " + name); 639 | }; 640 | 641 | /** 642 | * Match given `path` and return corresponding routes. 643 | * 644 | * @param {String} path 645 | * @param {String} method 646 | * @returns {Object.} returns layers that matched path and 647 | * path and method. 648 | * @private 649 | */ 650 | 651 | Router.prototype.match = function (path, method) { 652 | var layers = this.stack; 653 | var layer; 654 | var matched = { 655 | path: [], 656 | pathAndMethod: [], 657 | route: false 658 | }; 659 | 660 | for (var len = layers.length, i = 0; i < len; i++) { 661 | layer = layers[i]; 662 | 663 | debug('test %s %s', layer.path, layer.regexp); 664 | 665 | if (layer.match(path)) { 666 | matched.path.push(layer); 667 | 668 | if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) { 669 | matched.pathAndMethod.push(layer); 670 | if (layer.methods.length) matched.route = true; 671 | } 672 | } 673 | } 674 | 675 | return matched; 676 | }; 677 | 678 | /** 679 | * Run middleware for named route parameters. Useful for auto-loading or 680 | * validation. 681 | * 682 | * @example 683 | * 684 | * ```javascript 685 | * router 686 | * .param('user', (id, ctx, next) => { 687 | * ctx.user = users[id]; 688 | * if (!ctx.user) return ctx.status = 404; 689 | * return next(); 690 | * }) 691 | * .get('/users/:user', ctx => { 692 | * ctx.body = ctx.user; 693 | * }) 694 | * .get('/users/:user/friends', ctx => { 695 | * return ctx.user.getFriends().then(function(friends) { 696 | * ctx.body = friends; 697 | * }); 698 | * }) 699 | * // /users/3 => {"id": 3, "name": "Alex"} 700 | * // /users/3/friends => [{"id": 4, "name": "TJ"}] 701 | * ``` 702 | * 703 | * @param {String} param 704 | * @param {Function} middleware 705 | * @returns {Router} 706 | */ 707 | 708 | Router.prototype.param = function (param, middleware) { 709 | this.params[param] = middleware; 710 | this.stack.forEach(function (route) { 711 | route.param(param, middleware); 712 | }); 713 | return this; 714 | }; 715 | 716 | /** 717 | * Generate URL from url pattern and given `params`. 718 | * 719 | * @example 720 | * 721 | * ```javascript 722 | * var url = Router.url('/users/:id', {id: 1}); 723 | * // => "/users/1" 724 | * ``` 725 | * 726 | * @param {String} path url pattern 727 | * @param {Object} params url parameters 728 | * @returns {String} 729 | */ 730 | Router.url = function (path, params) { 731 | var args = Array.prototype.slice.call(arguments, 1); 732 | return Layer.prototype.url.apply({ path: path }, args); 733 | }; 734 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-router", 3 | "description": "Router middleware for koa. Provides RESTful resource routing.", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/alexmingoia/koa-router.git" 7 | }, 8 | "main": "lib/router.js", 9 | "files": [ 10 | "lib" 11 | ], 12 | "author": "Alex Mingoia ", 13 | "version": "7.4.0", 14 | "keywords": [ 15 | "koa", 16 | "middleware", 17 | "router", 18 | "route" 19 | ], 20 | "dependencies": { 21 | "debug": "^3.1.0", 22 | "http-errors": "^1.3.1", 23 | "koa-compose": "^3.0.0", 24 | "methods": "^1.0.1", 25 | "path-to-regexp": "^1.1.1", 26 | "urijs": "^1.19.0" 27 | }, 28 | "devDependencies": { 29 | "expect.js": "^0.3.1", 30 | "jsdoc-to-markdown": "^1.1.1", 31 | "koa": "2.2.0", 32 | "mocha": "^2.0.1", 33 | "should": "^6.0.3", 34 | "supertest": "^1.0.1" 35 | }, 36 | "scripts": { 37 | "test": "NODE_ENV=test mocha test/**/*.js", 38 | "docs": "NODE_ENV=test jsdoc2md -t ./lib/README_tpl.hbs --src ./lib/*.js >| README.md" 39 | }, 40 | "engines": { 41 | "node": ">= 4" 42 | }, 43 | "license": "MIT" 44 | } 45 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module tests 3 | */ 4 | 5 | var koa = require('koa') 6 | , should = require('should'); 7 | 8 | describe('module', function() { 9 | it('should expose Router', function(done) { 10 | var Router = require('..'); 11 | should.exist(Router); 12 | Router.should.be.type('function'); 13 | done(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/lib/layer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Route tests 3 | */ 4 | 5 | var Koa = require('koa') 6 | , http = require('http') 7 | , request = require('supertest') 8 | , Router = require('../../lib/router') 9 | , should = require('should') 10 | , Layer = require('../../lib/layer'); 11 | 12 | describe('Layer', function() { 13 | it('composes multiple callbacks/middlware', function(done) { 14 | var app = new Koa(); 15 | var router = new Router(); 16 | app.use(router.routes()); 17 | router.get( 18 | '/:category/:title', 19 | function (ctx, next) { 20 | ctx.status = 500; 21 | return next(); 22 | }, 23 | function (ctx, next) { 24 | ctx.status = 204; 25 | return next(); 26 | } 27 | ); 28 | request(http.createServer(app.callback())) 29 | .get('/programming/how-to-node') 30 | .expect(204) 31 | .end(function(err) { 32 | if (err) return done(err); 33 | done(); 34 | }); 35 | }); 36 | 37 | describe('Layer#match()', function() { 38 | it('captures URL path parameters', function(done) { 39 | var app = new Koa(); 40 | var router = new Router(); 41 | app.use(router.routes()); 42 | router.get('/:category/:title', function (ctx) { 43 | ctx.should.have.property('params'); 44 | ctx.params.should.be.type('object'); 45 | ctx.params.should.have.property('category', 'match'); 46 | ctx.params.should.have.property('title', 'this'); 47 | ctx.status = 204; 48 | }); 49 | request(http.createServer(app.callback())) 50 | .get('/match/this') 51 | .expect(204) 52 | .end(function(err, res) { 53 | if (err) return done(err); 54 | done(); 55 | }); 56 | }); 57 | 58 | it('return orginal path parameters when decodeURIComponent throw error', function(done) { 59 | var app = new Koa(); 60 | var router = new Router(); 61 | app.use(router.routes()); 62 | router.get('/:category/:title', function (ctx) { 63 | ctx.should.have.property('params'); 64 | ctx.params.should.be.type('object'); 65 | ctx.params.should.have.property('category', '100%'); 66 | ctx.params.should.have.property('title', '101%'); 67 | ctx.status = 204; 68 | }); 69 | request(http.createServer(app.callback())) 70 | .get('/100%/101%') 71 | .expect(204) 72 | .end(done); 73 | }); 74 | 75 | it('populates ctx.captures with regexp captures', function(done) { 76 | var app = new Koa(); 77 | var router = new Router(); 78 | app.use(router.routes()); 79 | router.get(/^\/api\/([^\/]+)\/?/i, function (ctx, next) { 80 | ctx.should.have.property('captures'); 81 | ctx.captures.should.be.instanceOf(Array); 82 | ctx.captures.should.have.property(0, '1'); 83 | return next(); 84 | }, function (ctx) { 85 | ctx.should.have.property('captures'); 86 | ctx.captures.should.be.instanceOf(Array); 87 | ctx.captures.should.have.property(0, '1'); 88 | ctx.status = 204; 89 | }); 90 | request(http.createServer(app.callback())) 91 | .get('/api/1') 92 | .expect(204) 93 | .end(function(err) { 94 | if (err) return done(err); 95 | done(); 96 | }); 97 | }); 98 | 99 | it('return orginal ctx.captures when decodeURIComponent throw error', function(done) { 100 | var app = new Koa(); 101 | var router = new Router(); 102 | app.use(router.routes()); 103 | router.get(/^\/api\/([^\/]+)\/?/i, function (ctx, next) { 104 | ctx.should.have.property('captures'); 105 | ctx.captures.should.be.type('object'); 106 | ctx.captures.should.have.property(0, '101%'); 107 | return next(); 108 | }, function (ctx, next) { 109 | ctx.should.have.property('captures'); 110 | ctx.captures.should.be.type('object'); 111 | ctx.captures.should.have.property(0, '101%'); 112 | ctx.status = 204; 113 | }); 114 | request(http.createServer(app.callback())) 115 | .get('/api/101%') 116 | .expect(204) 117 | .end(function(err) { 118 | if (err) return done(err); 119 | done(); 120 | }); 121 | }); 122 | 123 | it('populates ctx.captures with regexp captures include undefiend', function(done) { 124 | var app = new Koa(); 125 | var router = new Router(); 126 | app.use(router.routes()); 127 | router.get(/^\/api(\/.+)?/i, function (ctx, next) { 128 | ctx.should.have.property('captures'); 129 | ctx.captures.should.be.type('object'); 130 | ctx.captures.should.have.property(0, undefined); 131 | return next(); 132 | }, function (ctx) { 133 | ctx.should.have.property('captures'); 134 | ctx.captures.should.be.type('object'); 135 | ctx.captures.should.have.property(0, undefined); 136 | ctx.status = 204; 137 | }); 138 | request(http.createServer(app.callback())) 139 | .get('/api') 140 | .expect(204) 141 | .end(function(err) { 142 | if (err) return done(err); 143 | done(); 144 | }); 145 | }); 146 | 147 | it('should throw friendly error message when handle not exists', function() { 148 | var app = new Koa(); 149 | var router = new Router(); 150 | app.use(router.routes()); 151 | var notexistHandle = undefined; 152 | (function () { 153 | router.get('/foo', notexistHandle); 154 | }).should.throw('get `/foo`: `middleware` must be a function, not `undefined`'); 155 | 156 | (function () { 157 | router.get('foo router', '/foo', notexistHandle); 158 | }).should.throw('get `foo router`: `middleware` must be a function, not `undefined`'); 159 | 160 | (function () { 161 | router.post('/foo', function() {}, notexistHandle); 162 | }).should.throw('post `/foo`: `middleware` must be a function, not `undefined`'); 163 | }); 164 | }); 165 | 166 | describe('Layer#param()', function() { 167 | it('composes middleware for param fn', function(done) { 168 | var app = new Koa(); 169 | var router = new Router(); 170 | var route = new Layer('/users/:user', ['GET'], [function (ctx) { 171 | ctx.body = ctx.user; 172 | }]); 173 | route.param('user', function (id, ctx, next) { 174 | ctx.user = { name: 'alex' }; 175 | if (!id) return ctx.status = 404; 176 | return next(); 177 | }); 178 | router.stack.push(route); 179 | app.use(router.middleware()); 180 | request(http.createServer(app.callback())) 181 | .get('/users/3') 182 | .expect(200) 183 | .end(function(err, res) { 184 | if (err) return done(err); 185 | res.should.have.property('body'); 186 | res.body.should.have.property('name', 'alex'); 187 | done(); 188 | }); 189 | }); 190 | 191 | it('ignores params which are not matched', function(done) { 192 | var app = new Koa(); 193 | var router = new Router(); 194 | var route = new Layer('/users/:user', ['GET'], [function (ctx) { 195 | ctx.body = ctx.user; 196 | }]); 197 | route.param('user', function (id, ctx, next) { 198 | ctx.user = { name: 'alex' }; 199 | if (!id) return ctx.status = 404; 200 | return next(); 201 | }); 202 | route.param('title', function (id, ctx, next) { 203 | ctx.user = { name: 'mark' }; 204 | if (!id) return ctx.status = 404; 205 | return next(); 206 | }); 207 | router.stack.push(route); 208 | app.use(router.middleware()); 209 | request(http.createServer(app.callback())) 210 | .get('/users/3') 211 | .expect(200) 212 | .end(function(err, res) { 213 | if (err) return done(err); 214 | res.should.have.property('body'); 215 | res.body.should.have.property('name', 'alex'); 216 | done(); 217 | }); 218 | }); 219 | }); 220 | 221 | describe('Layer#url()', function() { 222 | it('generates route URL', function() { 223 | var route = new Layer('/:category/:title', ['get'], [function () {}], 'books'); 224 | var url = route.url({ category: 'programming', title: 'how-to-node' }); 225 | url.should.equal('/programming/how-to-node'); 226 | url = route.url('programming', 'how-to-node'); 227 | url.should.equal('/programming/how-to-node'); 228 | }); 229 | 230 | it('escapes using encodeURIComponent()', function() { 231 | var route = new Layer('/:category/:title', ['get'], [function () {}], 'books'); 232 | var url = route.url({ category: 'programming', title: 'how to node' }); 233 | url.should.equal('/programming/how%20to%20node'); 234 | }); 235 | }); 236 | }); 237 | -------------------------------------------------------------------------------- /test/lib/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Router tests 3 | */ 4 | 5 | var fs = require('fs') 6 | , http = require('http') 7 | , Koa = require('koa') 8 | , methods = require('methods') 9 | , path = require('path') 10 | , request = require('supertest') 11 | , Router = require('../../lib/router') 12 | , Layer = require('../../lib/layer') 13 | , expect = require('expect.js') 14 | , should = require('should'); 15 | 16 | describe('Router', function () { 17 | it('creates new router with koa app', function (done) { 18 | var app = new Koa(); 19 | var router = new Router(); 20 | router.should.be.instanceOf(Router); 21 | done(); 22 | }); 23 | 24 | it('shares context between routers (gh-205)', function (done) { 25 | var app = new Koa(); 26 | var router1 = new Router(); 27 | var router2 = new Router(); 28 | router1.get('/', function (ctx, next) { 29 | ctx.foo = 'bar'; 30 | return next(); 31 | }); 32 | router2.get('/', function (ctx, next) { 33 | ctx.baz = 'qux'; 34 | ctx.body = { foo: ctx.foo }; 35 | return next(); 36 | }); 37 | app.use(router1.routes()).use(router2.routes()); 38 | request(http.createServer(app.callback())) 39 | .get('/') 40 | .expect(200) 41 | .end(function (err, res) { 42 | if (err) return done(err); 43 | expect(res.body).to.have.property('foo', 'bar'); 44 | done(); 45 | }); 46 | }); 47 | 48 | it('does not register middleware more than once (gh-184)', function (done) { 49 | var app = new Koa(); 50 | var parentRouter = new Router(); 51 | var nestedRouter = new Router(); 52 | 53 | nestedRouter 54 | .get('/first-nested-route', function (ctx, next) { 55 | ctx.body = { n: ctx.n }; 56 | }) 57 | .get('/second-nested-route', function (ctx, next) { 58 | return next(); 59 | }) 60 | .get('/third-nested-route', function (ctx, next) { 61 | return next(); 62 | }); 63 | 64 | parentRouter.use('/parent-route', function (ctx, next) { 65 | ctx.n = ctx.n ? (ctx.n + 1) : 1; 66 | return next(); 67 | }, nestedRouter.routes()); 68 | 69 | app.use(parentRouter.routes()); 70 | 71 | request(http.createServer(app.callback())) 72 | .get('/parent-route/first-nested-route') 73 | .expect(200) 74 | .end(function (err, res) { 75 | if (err) return done(err); 76 | expect(res.body).to.have.property('n', 1); 77 | done(); 78 | }); 79 | }); 80 | 81 | it('router can be accecced with ctx', function (done) { 82 | var app = new Koa(); 83 | var router = new Router(); 84 | router.get('home', '/', function (ctx) { 85 | ctx.body = { 86 | url: ctx.router.url('home') 87 | }; 88 | }); 89 | app.use(router.routes()); 90 | request(http.createServer(app.callback())) 91 | .get('/') 92 | .expect(200) 93 | .end(function (err, res) { 94 | if (err) return done(err); 95 | expect(res.body.url).to.eql("/"); 96 | done(); 97 | }); 98 | }); 99 | 100 | it('registers multiple middleware for one route', function(done) { 101 | var app = new Koa(); 102 | var router = new Router(); 103 | 104 | router.get('/double', function(ctx, next) { 105 | return new Promise(function(resolve, reject) { 106 | setTimeout(function() { 107 | ctx.body = {message: 'Hello'}; 108 | resolve(next()); 109 | }, 1); 110 | }); 111 | }, function(ctx, next) { 112 | return new Promise(function(resolve, reject) { 113 | setTimeout(function() { 114 | ctx.body.message += ' World'; 115 | resolve(next()); 116 | }, 1); 117 | }); 118 | }, function(ctx, next) { 119 | ctx.body.message += '!'; 120 | }); 121 | 122 | app.use(router.routes()); 123 | 124 | request(http.createServer(app.callback())) 125 | .get('/double') 126 | .expect(200) 127 | .end(function (err, res) { 128 | if (err) return done(err); 129 | expect(res.body.message).to.eql('Hello World!'); 130 | done(); 131 | }); 132 | }); 133 | 134 | it('does not break when nested-routes use regexp paths', function (done) { 135 | var app = new Koa(); 136 | var parentRouter = new Router(); 137 | var nestedRouter = new Router(); 138 | 139 | nestedRouter 140 | .get(/^\/\w$/i, function (ctx, next) { 141 | return next(); 142 | }) 143 | .get('/first-nested-route', function (ctx, next) { 144 | return next(); 145 | }) 146 | .get('/second-nested-route', function (ctx, next) { 147 | return next(); 148 | }); 149 | 150 | parentRouter.use('/parent-route', function (ctx, next) { 151 | return next(); 152 | }, nestedRouter.routes()); 153 | 154 | app.use(parentRouter.routes()); 155 | app.should.be.ok; 156 | done(); 157 | }); 158 | 159 | it('exposes middleware factory', function (done) { 160 | var app = new Koa(); 161 | var router = new Router(); 162 | router.should.have.property('routes'); 163 | router.routes.should.be.type('function'); 164 | var middleware = router.routes(); 165 | should.exist(middleware); 166 | middleware.should.be.type('function'); 167 | done(); 168 | }); 169 | 170 | it('supports promises for async/await', function (done) { 171 | var app = new Koa(); 172 | app.experimental = true; 173 | var router = Router(); 174 | router.get('/async', function (ctx, next) { 175 | return new Promise(function (resolve, reject) { 176 | setTimeout(function() { 177 | ctx.body = { 178 | msg: 'promises!' 179 | }; 180 | resolve(); 181 | }, 1); 182 | }); 183 | }); 184 | 185 | app.use(router.routes()).use(router.allowedMethods()); 186 | request(http.createServer(app.callback())) 187 | .get('/async') 188 | .expect(200) 189 | .end(function (err, res) { 190 | if (err) return done(err); 191 | expect(res.body).to.have.property('msg', 'promises!'); 192 | done(); 193 | }); 194 | }); 195 | 196 | it('matches middleware only if route was matched (gh-182)', function (done) { 197 | var app = new Koa(); 198 | var router = new Router(); 199 | var otherRouter = new Router(); 200 | 201 | router.use(function (ctx, next) { 202 | ctx.body = { bar: 'baz' }; 203 | return next(); 204 | }); 205 | 206 | otherRouter.get('/bar', function (ctx) { 207 | ctx.body = ctx.body || { foo: 'bar' }; 208 | }); 209 | 210 | app.use(router.routes()).use(otherRouter.routes()); 211 | 212 | request(http.createServer(app.callback())) 213 | .get('/bar') 214 | .expect(200) 215 | .end(function (err, res) { 216 | if (err) return done(err); 217 | expect(res.body).to.have.property('foo', 'bar'); 218 | expect(res.body).to.not.have.property('bar'); 219 | done(); 220 | }) 221 | }); 222 | 223 | it('matches first to last', function (done) { 224 | var app = new Koa(); 225 | var router = new Router(); 226 | 227 | router 228 | .get('user_page', '/user/(.*).jsx', function (ctx) { 229 | ctx.body = { order: 1 }; 230 | }) 231 | .all('app', '/app/(.*).jsx', function (ctx) { 232 | ctx.body = { order: 2 }; 233 | }) 234 | .all('view', '(.*).jsx', function (ctx) { 235 | ctx.body = { order: 3 }; 236 | }); 237 | 238 | request(http.createServer(app.use(router.routes()).callback())) 239 | .get('/user/account.jsx') 240 | .expect(200) 241 | .end(function (err, res) { 242 | if (err) return done(err); 243 | expect(res.body).to.have.property('order', 1); 244 | done(); 245 | }) 246 | }); 247 | 248 | it('does not run subsequent middleware without calling next', function (done) { 249 | var app = new Koa(); 250 | var router = new Router(); 251 | 252 | router 253 | .get('user_page', '/user/(.*).jsx', function (ctx) { 254 | // no next() 255 | }, function (ctx) { 256 | ctx.body = { order: 1 }; 257 | }); 258 | 259 | request(http.createServer(app.use(router.routes()).callback())) 260 | .get('/user/account.jsx') 261 | .expect(404) 262 | .end(done) 263 | }); 264 | 265 | it('nests routers with prefixes at root', function (done) { 266 | var app = new Koa(); 267 | var api = new Router(); 268 | var forums = new Router({ 269 | prefix: '/forums' 270 | }); 271 | var posts = new Router({ 272 | prefix: '/:fid/posts' 273 | }); 274 | var server; 275 | 276 | posts 277 | .get('/', function (ctx, next) { 278 | ctx.status = 204; 279 | return next(); 280 | }) 281 | .get('/:pid', function (ctx, next) { 282 | ctx.body = ctx.params; 283 | return next(); 284 | }); 285 | 286 | forums.use(posts.routes()); 287 | 288 | server = http.createServer(app.use(forums.routes()).callback()); 289 | 290 | request(server) 291 | .get('/forums/1/posts') 292 | .expect(204) 293 | .end(function (err) { 294 | if (err) return done(err); 295 | 296 | request(server) 297 | .get('/forums/1') 298 | .expect(404) 299 | .end(function (err) { 300 | if (err) return done(err); 301 | 302 | request(server) 303 | .get('/forums/1/posts/2') 304 | .expect(200) 305 | .end(function (err, res) { 306 | if (err) return done(err); 307 | 308 | expect(res.body).to.have.property('fid', '1'); 309 | expect(res.body).to.have.property('pid', '2'); 310 | done(); 311 | }); 312 | }); 313 | }); 314 | }); 315 | 316 | it('nests routers with prefixes at path', function (done) { 317 | var app = new Koa(); 318 | var api = new Router(); 319 | var forums = new Router({ 320 | prefix: '/api' 321 | }); 322 | var posts = new Router({ 323 | prefix: '/posts' 324 | }); 325 | var server; 326 | 327 | posts 328 | .get('/', function (ctx, next) { 329 | ctx.status = 204; 330 | return next(); 331 | }) 332 | .get('/:pid', function (ctx, next) { 333 | ctx.body = ctx.params; 334 | return next(); 335 | }); 336 | 337 | forums.use('/forums/:fid', posts.routes()); 338 | 339 | server = http.createServer(app.use(forums.routes()).callback()); 340 | 341 | request(server) 342 | .get('/api/forums/1/posts') 343 | .expect(204) 344 | .end(function (err) { 345 | if (err) return done(err); 346 | 347 | request(server) 348 | .get('/api/forums/1') 349 | .expect(404) 350 | .end(function (err) { 351 | if (err) return done(err); 352 | 353 | request(server) 354 | .get('/api/forums/1/posts/2') 355 | .expect(200) 356 | .end(function (err, res) { 357 | if (err) return done(err); 358 | 359 | expect(res.body).to.have.property('fid', '1'); 360 | expect(res.body).to.have.property('pid', '2'); 361 | done(); 362 | }); 363 | }); 364 | }); 365 | }); 366 | 367 | it('runs subrouter middleware after parent', function (done) { 368 | var app = new Koa(); 369 | var subrouter = Router() 370 | .use(function (ctx, next) { 371 | ctx.msg = 'subrouter'; 372 | return next(); 373 | }) 374 | .get('/', function (ctx) { 375 | ctx.body = { msg: ctx.msg }; 376 | }); 377 | var router = Router() 378 | .use(function (ctx, next) { 379 | ctx.msg = 'router'; 380 | return next(); 381 | }) 382 | .use(subrouter.routes()); 383 | request(http.createServer(app.use(router.routes()).callback())) 384 | .get('/') 385 | .expect(200) 386 | .end(function (err, res) { 387 | if (err) return done(err); 388 | expect(res.body).to.have.property('msg', 'subrouter'); 389 | done(); 390 | }); 391 | }); 392 | 393 | it('runs parent middleware for subrouter routes', function (done) { 394 | var app = new Koa(); 395 | var subrouter = Router() 396 | .get('/sub', function (ctx) { 397 | ctx.body = { msg: ctx.msg }; 398 | }); 399 | var router = Router() 400 | .use(function (ctx, next) { 401 | ctx.msg = 'router'; 402 | return next(); 403 | }) 404 | .use('/parent', subrouter.routes()); 405 | request(http.createServer(app.use(router.routes()).callback())) 406 | .get('/parent/sub') 407 | .expect(200) 408 | .end(function (err, res) { 409 | if (err) return done(err); 410 | expect(res.body).to.have.property('msg', 'router'); 411 | done(); 412 | }); 413 | }); 414 | 415 | it('matches corresponding requests', function (done) { 416 | var app = new Koa(); 417 | var router = new Router(); 418 | app.use(router.routes()); 419 | router.get('/:category/:title', function (ctx) { 420 | ctx.should.have.property('params'); 421 | ctx.params.should.have.property('category', 'programming'); 422 | ctx.params.should.have.property('title', 'how-to-node'); 423 | ctx.status = 204; 424 | }); 425 | router.post('/:category', function (ctx) { 426 | ctx.should.have.property('params'); 427 | ctx.params.should.have.property('category', 'programming'); 428 | ctx.status = 204; 429 | }); 430 | router.put('/:category/not-a-title', function (ctx) { 431 | ctx.should.have.property('params'); 432 | ctx.params.should.have.property('category', 'programming'); 433 | ctx.params.should.not.have.property('title'); 434 | ctx.status = 204; 435 | }); 436 | var server = http.createServer(app.callback()); 437 | request(server) 438 | .get('/programming/how-to-node') 439 | .expect(204) 440 | .end(function (err, res) { 441 | if (err) return done(err); 442 | request(server) 443 | .post('/programming') 444 | .expect(204) 445 | .end(function (err, res) { 446 | if (err) return done(err); 447 | request(server) 448 | .put('/programming/not-a-title') 449 | .expect(204) 450 | .end(function (err, res) { 451 | done(err); 452 | }); 453 | }); 454 | }); 455 | }); 456 | 457 | it('executes route middleware using `app.context`', function (done) { 458 | var app = new Koa(); 459 | var router = new Router(); 460 | app.use(router.routes()); 461 | router.use(function (ctx, next) { 462 | ctx.bar = 'baz'; 463 | return next(); 464 | }); 465 | router.get('/:category/:title', function (ctx, next) { 466 | ctx.foo = 'bar'; 467 | return next(); 468 | }, function (ctx) { 469 | ctx.should.have.property('bar', 'baz'); 470 | ctx.should.have.property('foo', 'bar'); 471 | ctx.should.have.property('app'); 472 | ctx.should.have.property('req'); 473 | ctx.should.have.property('res'); 474 | ctx.status = 204; 475 | done(); 476 | }); 477 | request(http.createServer(app.callback())) 478 | .get('/match/this') 479 | .expect(204) 480 | .end(function (err) { 481 | if (err) return done(err); 482 | }); 483 | }); 484 | 485 | it('does not match after ctx.throw()', function (done) { 486 | var app = new Koa(); 487 | var counter = 0; 488 | var router = new Router(); 489 | app.use(router.routes()); 490 | router.get('/', function (ctx) { 491 | counter++; 492 | ctx.throw(403); 493 | }); 494 | router.get('/', function () { 495 | counter++; 496 | }); 497 | var server = http.createServer(app.callback()); 498 | request(server) 499 | .get('/') 500 | .expect(403) 501 | .end(function (err, res) { 502 | if (err) return done(err); 503 | counter.should.equal(1); 504 | done(); 505 | }); 506 | }); 507 | 508 | it('supports promises for route middleware', function (done) { 509 | var app = new Koa(); 510 | var router = new Router(); 511 | app.use(router.routes()); 512 | var readVersion = function () { 513 | return new Promise(function (resolve, reject) { 514 | var packagePath = path.join(__dirname, '..', '..', 'package.json'); 515 | fs.readFile(packagePath, 'utf8', function (err, data) { 516 | if (err) return reject(err); 517 | resolve(JSON.parse(data).version); 518 | }); 519 | }); 520 | }; 521 | router 522 | .get('/', function (ctx, next) { 523 | return next(); 524 | }, function (ctx) { 525 | return readVersion().then(function () { 526 | ctx.status = 204; 527 | }); 528 | }); 529 | request(http.createServer(app.callback())) 530 | .get('/') 531 | .expect(204) 532 | .end(done); 533 | }); 534 | 535 | describe('Router#allowedMethods()', function () { 536 | it('responds to OPTIONS requests', function (done) { 537 | var app = new Koa(); 538 | var router = new Router(); 539 | app.use(router.routes()); 540 | app.use(router.allowedMethods()); 541 | router.get('/users', function (ctx, next) {}); 542 | router.put('/users', function (ctx, next) {}); 543 | request(http.createServer(app.callback())) 544 | .options('/users') 545 | .expect(200) 546 | .end(function (err, res) { 547 | if (err) return done(err); 548 | res.header.should.have.property('content-length', '0'); 549 | res.header.should.have.property('allow', 'HEAD, GET, PUT'); 550 | done(); 551 | }); 552 | }); 553 | 554 | it('responds with 405 Method Not Allowed', function (done) { 555 | var app = new Koa(); 556 | var router = new Router(); 557 | router.get('/users', function () {}); 558 | router.put('/users', function () {}); 559 | router.post('/events', function () {}); 560 | app.use(router.routes()); 561 | app.use(router.allowedMethods()); 562 | request(http.createServer(app.callback())) 563 | .post('/users') 564 | .expect(405) 565 | .end(function (err, res) { 566 | if (err) return done(err); 567 | res.header.should.have.property('allow', 'HEAD, GET, PUT'); 568 | done(); 569 | }); 570 | }); 571 | 572 | it('responds with 405 Method Not Allowed using the "throw" option', function (done) { 573 | var app = new Koa(); 574 | var router = new Router(); 575 | app.use(router.routes()); 576 | app.use(function (ctx, next) { 577 | return next().catch(function (err) { 578 | // assert that the correct HTTPError was thrown 579 | err.name.should.equal('MethodNotAllowedError'); 580 | err.statusCode.should.equal(405); 581 | 582 | // translate the HTTPError to a normal response 583 | ctx.body = err.name; 584 | ctx.status = err.statusCode; 585 | }); 586 | }); 587 | app.use(router.allowedMethods({ throw: true })); 588 | router.get('/users', function () {}); 589 | router.put('/users', function () {}); 590 | router.post('/events', function () {}); 591 | request(http.createServer(app.callback())) 592 | .post('/users') 593 | .expect(405) 594 | .end(function (err, res) { 595 | if (err) return done(err); 596 | // the 'Allow' header is not set when throwing 597 | res.header.should.not.have.property('allow'); 598 | done(); 599 | }); 600 | }); 601 | 602 | it('responds with user-provided throwable using the "throw" and "methodNotAllowed" options', function (done) { 603 | var app = new Koa(); 604 | var router = new Router(); 605 | app.use(router.routes()); 606 | app.use(function (ctx, next) { 607 | return next().catch(function (err) { 608 | // assert that the correct HTTPError was thrown 609 | err.message.should.equal('Custom Not Allowed Error'); 610 | err.statusCode.should.equal(405); 611 | 612 | // translate the HTTPError to a normal response 613 | ctx.body = err.body; 614 | ctx.status = err.statusCode; 615 | }); 616 | }); 617 | app.use(router.allowedMethods({ 618 | throw: true, 619 | methodNotAllowed: function () { 620 | var notAllowedErr = new Error('Custom Not Allowed Error'); 621 | notAllowedErr.type = 'custom'; 622 | notAllowedErr.statusCode = 405; 623 | notAllowedErr.body = { 624 | error: 'Custom Not Allowed Error', 625 | statusCode: 405, 626 | otherStuff: true 627 | }; 628 | return notAllowedErr; 629 | } 630 | })); 631 | router.get('/users', function () {}); 632 | router.put('/users', function () {}); 633 | router.post('/events', function () {}); 634 | request(http.createServer(app.callback())) 635 | .post('/users') 636 | .expect(405) 637 | .end(function (err, res) { 638 | if (err) return done(err); 639 | // the 'Allow' header is not set when throwing 640 | res.header.should.not.have.property('allow'); 641 | res.body.should.eql({ error: 'Custom Not Allowed Error', 642 | statusCode: 405, 643 | otherStuff: true 644 | }); 645 | done(); 646 | }); 647 | }); 648 | 649 | it('responds with 501 Not Implemented', function (done) { 650 | var app = new Koa(); 651 | var router = new Router(); 652 | app.use(router.routes()); 653 | app.use(router.allowedMethods()); 654 | router.get('/users', function () {}); 655 | router.put('/users', function () {}); 656 | request(http.createServer(app.callback())) 657 | .search('/users') 658 | .expect(501) 659 | .end(function (err, res) { 660 | if (err) return done(err); 661 | done(); 662 | }); 663 | }); 664 | 665 | it('responds with 501 Not Implemented using the "throw" option', function (done) { 666 | var app = new Koa(); 667 | var router = new Router(); 668 | app.use(router.routes()); 669 | app.use(function (ctx, next) { 670 | return next().catch(function (err) { 671 | // assert that the correct HTTPError was thrown 672 | err.name.should.equal('NotImplementedError'); 673 | err.statusCode.should.equal(501); 674 | 675 | // translate the HTTPError to a normal response 676 | ctx.body = err.name; 677 | ctx.status = err.statusCode; 678 | }); 679 | }); 680 | app.use(router.allowedMethods({ throw: true })); 681 | router.get('/users', function () {}); 682 | router.put('/users', function () {}); 683 | request(http.createServer(app.callback())) 684 | .search('/users') 685 | .expect(501) 686 | .end(function (err, res) { 687 | if (err) return done(err); 688 | // the 'Allow' header is not set when throwing 689 | res.header.should.not.have.property('allow'); 690 | done(); 691 | }); 692 | }); 693 | 694 | it('responds with user-provided throwable using the "throw" and "notImplemented" options', function (done) { 695 | var app = new Koa(); 696 | var router = new Router(); 697 | app.use(router.routes()); 698 | app.use(function (ctx, next) { 699 | return next().catch(function (err) { 700 | // assert that our custom error was thrown 701 | err.message.should.equal('Custom Not Implemented Error'); 702 | err.type.should.equal('custom'); 703 | err.statusCode.should.equal(501); 704 | 705 | // translate the HTTPError to a normal response 706 | ctx.body = err.body; 707 | ctx.status = err.statusCode; 708 | }); 709 | }); 710 | app.use(router.allowedMethods({ 711 | throw: true, 712 | notImplemented: function () { 713 | var notImplementedErr = new Error('Custom Not Implemented Error'); 714 | notImplementedErr.type = 'custom'; 715 | notImplementedErr.statusCode = 501; 716 | notImplementedErr.body = { 717 | error: 'Custom Not Implemented Error', 718 | statusCode: 501, 719 | otherStuff: true 720 | }; 721 | return notImplementedErr; 722 | } 723 | })); 724 | router.get('/users', function () {}); 725 | router.put('/users', function () {}); 726 | request(http.createServer(app.callback())) 727 | .search('/users') 728 | .expect(501) 729 | .end(function (err, res) { 730 | if (err) return done(err); 731 | // the 'Allow' header is not set when throwing 732 | res.header.should.not.have.property('allow'); 733 | res.body.should.eql({ error: 'Custom Not Implemented Error', 734 | statusCode: 501, 735 | otherStuff: true 736 | }); 737 | done(); 738 | }); 739 | }); 740 | 741 | it('does not send 405 if route matched but status is 404', function (done) { 742 | var app = new Koa(); 743 | var router = new Router(); 744 | app.use(router.routes()); 745 | app.use(router.allowedMethods()); 746 | router.get('/users', function (ctx, next) { 747 | ctx.status = 404; 748 | }); 749 | request(http.createServer(app.callback())) 750 | .get('/users') 751 | .expect(404) 752 | .end(function (err, res) { 753 | if (err) return done(err); 754 | done(); 755 | }); 756 | }); 757 | 758 | it('sets the allowed methods to a single Allow header #273', function (done) { 759 | // https://tools.ietf.org/html/rfc7231#section-7.4.1 760 | var app = new Koa(); 761 | var router = new Router(); 762 | app.use(router.routes()); 763 | app.use(router.allowedMethods()); 764 | 765 | router.get('/', function (ctx, next) {}); 766 | 767 | request(http.createServer(app.callback())) 768 | .options('/') 769 | .expect(200) 770 | .end(function (err, res) { 771 | if (err) return done(err); 772 | res.header.should.have.property('allow', 'HEAD, GET'); 773 | let allowHeaders = res.res.rawHeaders.filter((item) => item == 'Allow'); 774 | expect(allowHeaders.length).to.eql(1); 775 | done(); 776 | }); 777 | }); 778 | 779 | }); 780 | 781 | it('supports custom routing detect path: ctx.routerPath', function (done) { 782 | var app = new Koa(); 783 | var router = new Router(); 784 | app.use(function (ctx, next) { 785 | // bind helloworld.example.com/users => example.com/helloworld/users 786 | var appname = ctx.request.hostname.split('.', 1)[0]; 787 | ctx.routerPath = '/' + appname + ctx.path; 788 | return next(); 789 | }); 790 | app.use(router.routes()); 791 | router.get('/helloworld/users', function (ctx) { 792 | ctx.body = ctx.method + ' ' + ctx.url; 793 | }); 794 | 795 | request(http.createServer(app.callback())) 796 | .get('/users') 797 | .set('Host', 'helloworld.example.com') 798 | .expect(200) 799 | .expect('GET /users', done); 800 | }); 801 | 802 | describe('Router#[verb]()', function () { 803 | it('registers route specific to HTTP verb', function () { 804 | var app = new Koa(); 805 | var router = new Router(); 806 | app.use(router.routes()); 807 | methods.forEach(function (method) { 808 | router.should.have.property(method); 809 | router[method].should.be.type('function'); 810 | router[method]('/', function () {}); 811 | }); 812 | router.stack.should.have.length(methods.length); 813 | }); 814 | 815 | it('registers route with a regexp path', function () { 816 | var router = new Router(); 817 | methods.forEach(function (method) { 818 | router[method](/^\/\w$/i, function () {}).should.equal(router); 819 | }); 820 | }); 821 | 822 | it('registers route with a given name', function () { 823 | var router = new Router(); 824 | methods.forEach(function (method) { 825 | router[method](method, '/', function () {}).should.equal(router); 826 | }); 827 | }); 828 | 829 | it('registers route with with a given name and regexp path', function () { 830 | var router = new Router(); 831 | methods.forEach(function (method) { 832 | router[method](method, /^\/$/i, function () {}).should.equal(router); 833 | }); 834 | }); 835 | 836 | it('enables route chaining', function () { 837 | var router = new Router(); 838 | methods.forEach(function (method) { 839 | router[method]('/', function () {}).should.equal(router); 840 | }); 841 | }); 842 | 843 | it('registers array of paths (gh-203)', function () { 844 | var router = new Router(); 845 | router.get(['/one', '/two'], function (ctx, next) { 846 | return next(); 847 | }); 848 | expect(router.stack).to.have.property('length', 2); 849 | expect(router.stack[0]).to.have.property('path', '/one'); 850 | expect(router.stack[1]).to.have.property('path', '/two'); 851 | }); 852 | 853 | it('resolves non-parameterized routes without attached parameters', function(done) { 854 | var app = new Koa(); 855 | var router = new Router(); 856 | 857 | router.get('/notparameter', function (ctx, next) { 858 | ctx.body = { 859 | param: ctx.params.parameter, 860 | }; 861 | }); 862 | 863 | router.get('/:parameter', function (ctx, next) { 864 | ctx.body = { 865 | param: ctx.params.parameter, 866 | }; 867 | }); 868 | 869 | app.use(router.routes()); 870 | request(http.createServer(app.callback())) 871 | .get('/notparameter') 872 | .expect(200) 873 | .end(function (err, res) { 874 | if (err) return done(err); 875 | 876 | expect(res.body.param).to.equal(undefined); 877 | done(); 878 | }); 879 | }); 880 | 881 | }); 882 | 883 | describe('Router#use()', function (done) { 884 | it('uses router middleware without path', function (done) { 885 | var app = new Koa(); 886 | var router = new Router(); 887 | 888 | router.use(function (ctx, next) { 889 | ctx.foo = 'baz'; 890 | return next(); 891 | }); 892 | 893 | router.use(function (ctx, next) { 894 | ctx.foo = 'foo'; 895 | return next(); 896 | }); 897 | 898 | router.get('/foo/bar', function (ctx) { 899 | ctx.body = { 900 | foobar: ctx.foo + 'bar' 901 | }; 902 | }); 903 | 904 | app.use(router.routes()); 905 | request(http.createServer(app.callback())) 906 | .get('/foo/bar') 907 | .expect(200) 908 | .end(function (err, res) { 909 | if (err) return done(err); 910 | 911 | expect(res.body).to.have.property('foobar', 'foobar'); 912 | done(); 913 | }); 914 | }); 915 | 916 | it('uses router middleware at given path', function (done) { 917 | var app = new Koa(); 918 | var router = new Router(); 919 | 920 | router.use('/foo/bar', function (ctx, next) { 921 | ctx.foo = 'foo'; 922 | return next(); 923 | }); 924 | 925 | router.get('/foo/bar', function (ctx) { 926 | ctx.body = { 927 | foobar: ctx.foo + 'bar' 928 | }; 929 | }); 930 | 931 | app.use(router.routes()); 932 | request(http.createServer(app.callback())) 933 | .get('/foo/bar') 934 | .expect(200) 935 | .end(function (err, res) { 936 | if (err) return done(err); 937 | 938 | expect(res.body).to.have.property('foobar', 'foobar'); 939 | done(); 940 | }); 941 | }); 942 | 943 | it('runs router middleware before subrouter middleware', function (done) { 944 | var app = new Koa(); 945 | var router = new Router(); 946 | var subrouter = new Router(); 947 | 948 | router.use(function (ctx, next) { 949 | ctx.foo = 'boo'; 950 | return next(); 951 | }); 952 | 953 | subrouter 954 | .use(function (ctx, next) { 955 | ctx.foo = 'foo'; 956 | return next(); 957 | }) 958 | .get('/bar', function (ctx) { 959 | ctx.body = { 960 | foobar: ctx.foo + 'bar' 961 | }; 962 | }); 963 | 964 | router.use('/foo', subrouter.routes()); 965 | app.use(router.routes()); 966 | request(http.createServer(app.callback())) 967 | .get('/foo/bar') 968 | .expect(200) 969 | .end(function (err, res) { 970 | if (err) return done(err); 971 | 972 | expect(res.body).to.have.property('foobar', 'foobar'); 973 | done(); 974 | }); 975 | }); 976 | 977 | it('assigns middleware to array of paths', function (done) { 978 | var app = new Koa(); 979 | var router = new Router(); 980 | 981 | router.use(['/foo', '/bar'], function (ctx, next) { 982 | ctx.foo = 'foo'; 983 | ctx.bar = 'bar'; 984 | return next(); 985 | }); 986 | 987 | router.get('/foo', function (ctx, next) { 988 | ctx.body = { 989 | foobar: ctx.foo + 'bar' 990 | }; 991 | }); 992 | 993 | router.get('/bar', function (ctx) { 994 | ctx.body = { 995 | foobar: 'foo' + ctx.bar 996 | }; 997 | }); 998 | 999 | app.use(router.routes()); 1000 | request(http.createServer(app.callback())) 1001 | .get('/foo') 1002 | .expect(200) 1003 | .end(function (err, res) { 1004 | if (err) return done(err); 1005 | expect(res.body).to.have.property('foobar', 'foobar'); 1006 | request(http.createServer(app.callback())) 1007 | .get('/bar') 1008 | .expect(200) 1009 | .end(function (err, res) { 1010 | if (err) return done(err); 1011 | expect(res.body).to.have.property('foobar', 'foobar'); 1012 | done(); 1013 | }); 1014 | }); 1015 | }); 1016 | 1017 | it('without path, does not set params.0 to the matched path - gh-247', function (done) { 1018 | var app = new Koa(); 1019 | var router = new Router(); 1020 | 1021 | router.use(function(ctx, next) { 1022 | return next(); 1023 | }); 1024 | 1025 | router.get('/foo/:id', function(ctx) { 1026 | ctx.body = ctx.params; 1027 | }); 1028 | 1029 | app.use(router.routes()); 1030 | request(http.createServer(app.callback())) 1031 | .get('/foo/815') 1032 | .expect(200) 1033 | .end(function (err, res) { 1034 | if (err) return done(err); 1035 | 1036 | expect(res.body).to.have.property('id', '815'); 1037 | expect(res.body).to.not.have.property('0'); 1038 | done(); 1039 | }); 1040 | }); 1041 | 1042 | it('does not add an erroneous (.*) to unprefiexed nested routers - gh-369 gh-410', function (done) { 1043 | var app = new Koa(); 1044 | var router = new Router(); 1045 | var nested = new Router(); 1046 | var called = 0; 1047 | 1048 | nested 1049 | .get('/', (ctx, next) => { 1050 | ctx.body = 'root'; 1051 | called += 1; 1052 | return next(); 1053 | }) 1054 | .get('/test', (ctx, next) => { 1055 | ctx.body = 'test'; 1056 | called += 1; 1057 | return next(); 1058 | }); 1059 | 1060 | router.use(nested.routes()); 1061 | app.use(router.routes()); 1062 | 1063 | request(app.callback()) 1064 | .get('/test') 1065 | .expect(200) 1066 | .expect('test') 1067 | .end(function (err, res) { 1068 | if (err) return done(err); 1069 | expect(called).to.eql(1, 'too many routes matched'); 1070 | done(); 1071 | }); 1072 | }); 1073 | }); 1074 | 1075 | describe('Router#register()', function () { 1076 | it('registers new routes', function (done) { 1077 | var app = new Koa(); 1078 | var router = new Router(); 1079 | router.should.have.property('register'); 1080 | router.register.should.be.type('function'); 1081 | var route = router.register('/', ['GET', 'POST'], function () {}); 1082 | app.use(router.routes()); 1083 | router.stack.should.be.an.instanceOf(Array); 1084 | router.stack.should.have.property('length', 1); 1085 | router.stack[0].should.have.property('path', '/'); 1086 | done(); 1087 | }); 1088 | }); 1089 | 1090 | describe('Router#redirect()', function () { 1091 | it('registers redirect routes', function (done) { 1092 | var app = new Koa(); 1093 | var router = new Router(); 1094 | router.should.have.property('redirect'); 1095 | router.redirect.should.be.type('function'); 1096 | router.redirect('/source', '/destination', 302); 1097 | app.use(router.routes()); 1098 | router.stack.should.have.property('length', 1); 1099 | router.stack[0].should.be.instanceOf(Layer); 1100 | router.stack[0].should.have.property('path', '/source'); 1101 | done(); 1102 | }); 1103 | 1104 | it('redirects using route names', function (done) { 1105 | var app = new Koa(); 1106 | var router = new Router(); 1107 | app.use(router.routes()); 1108 | router.get('home', '/', function () {}); 1109 | router.get('sign-up-form', '/sign-up-form', function () {}); 1110 | router.redirect('home', 'sign-up-form'); 1111 | request(http.createServer(app.callback())) 1112 | .post('/') 1113 | .expect(301) 1114 | .end(function (err, res) { 1115 | if (err) return done(err); 1116 | res.header.should.have.property('location', '/sign-up-form'); 1117 | done(); 1118 | }); 1119 | }); 1120 | }); 1121 | 1122 | describe('Router#route()', function () { 1123 | it('inherits routes from nested router', function () { 1124 | var app = new Koa(); 1125 | var subrouter = Router().get('child', '/hello', function (ctx) { 1126 | ctx.body = { hello: 'world' }; 1127 | }); 1128 | var router = Router().use(subrouter.routes()); 1129 | expect(router.route('child')).to.have.property('name', 'child'); 1130 | }); 1131 | }); 1132 | 1133 | describe('Router#url()', function () { 1134 | it('generates URL for given route name', function (done) { 1135 | var app = new Koa(); 1136 | var router = new Router(); 1137 | app.use(router.routes()); 1138 | router.get('books', '/:category/:title', function (ctx) { 1139 | ctx.status = 204; 1140 | }); 1141 | var url = router.url('books', { category: 'programming', title: 'how to node' }); 1142 | url.should.equal('/programming/how%20to%20node'); 1143 | url = router.url('books', 'programming', 'how to node'); 1144 | url.should.equal('/programming/how%20to%20node'); 1145 | done(); 1146 | }); 1147 | 1148 | it('generates URL for given route name within embedded routers', function (done) { 1149 | var app = new Koa(); 1150 | var router = new Router({ 1151 | prefix: "/books" 1152 | }); 1153 | 1154 | var embeddedRouter = new Router({ 1155 | prefix: "/chapters" 1156 | }); 1157 | embeddedRouter.get('chapters', '/:chapterName/:pageNumber', function (ctx) { 1158 | ctx.status = 204; 1159 | }); 1160 | router.use(embeddedRouter.routes()); 1161 | app.use(router.routes()); 1162 | var url = router.url('chapters', { chapterName: 'Learning ECMA6', pageNumber: 123 }); 1163 | url.should.equal('/books/chapters/Learning%20ECMA6/123'); 1164 | url = router.url('chapters', 'Learning ECMA6', 123); 1165 | url.should.equal('/books/chapters/Learning%20ECMA6/123'); 1166 | done(); 1167 | }); 1168 | 1169 | it('generates URL for given route name within two embedded routers', function (done) { 1170 | var app = new Koa(); 1171 | var router = new Router({ 1172 | prefix: "/books" 1173 | }); 1174 | var embeddedRouter = new Router({ 1175 | prefix: "/chapters" 1176 | }); 1177 | var embeddedRouter2 = new Router({ 1178 | prefix: "/:chapterName/pages" 1179 | }); 1180 | embeddedRouter2.get('chapters', '/:pageNumber', function (ctx) { 1181 | ctx.status = 204; 1182 | }); 1183 | embeddedRouter.use(embeddedRouter2.routes()); 1184 | router.use(embeddedRouter.routes()); 1185 | app.use(router.routes()); 1186 | var url = router.url('chapters', { chapterName: 'Learning ECMA6', pageNumber: 123 }); 1187 | url.should.equal('/books/chapters/Learning%20ECMA6/pages/123'); 1188 | done(); 1189 | }); 1190 | 1191 | it('generates URL for given route name with params and query params', function(done) { 1192 | var app = new Koa(); 1193 | var router = new Router(); 1194 | router.get('books', '/books/:category/:id', function (ctx) { 1195 | ctx.status = 204; 1196 | }); 1197 | var url = router.url('books', 'programming', 4, { 1198 | query: { page: 3, limit: 10 } 1199 | }); 1200 | url.should.equal('/books/programming/4?page=3&limit=10'); 1201 | var url = router.url('books', 1202 | { category: 'programming', id: 4 }, 1203 | { query: { page: 3, limit: 10 }} 1204 | ); 1205 | url.should.equal('/books/programming/4?page=3&limit=10'); 1206 | var url = router.url('books', 1207 | { category: 'programming', id: 4 }, 1208 | { query: 'page=3&limit=10' } 1209 | ); 1210 | url.should.equal('/books/programming/4?page=3&limit=10'); 1211 | done(); 1212 | }) 1213 | 1214 | 1215 | it('generates URL for given route name without params and query params', function(done) { 1216 | var app = new Koa(); 1217 | var router = new Router(); 1218 | router.get('category', '/category', function (ctx) { 1219 | ctx.status = 204; 1220 | }); 1221 | var url = router.url('category', { 1222 | query: { page: 3, limit: 10 } 1223 | }); 1224 | url.should.equal('/category?page=3&limit=10'); 1225 | done(); 1226 | }) 1227 | }); 1228 | 1229 | describe('Router#param()', function () { 1230 | it('runs parameter middleware', function (done) { 1231 | var app = new Koa(); 1232 | var router = new Router(); 1233 | app.use(router.routes()); 1234 | router 1235 | .param('user', function (id, ctx, next) { 1236 | ctx.user = { name: 'alex' }; 1237 | if (!id) return ctx.status = 404; 1238 | return next(); 1239 | }) 1240 | .get('/users/:user', function (ctx, next) { 1241 | ctx.body = ctx.user; 1242 | }); 1243 | request(http.createServer(app.callback())) 1244 | .get('/users/3') 1245 | .expect(200) 1246 | .end(function (err, res) { 1247 | if (err) return done(err); 1248 | res.should.have.property('body'); 1249 | res.body.should.have.property('name', 'alex'); 1250 | done(); 1251 | }); 1252 | }); 1253 | 1254 | it('runs parameter middleware in order of URL appearance', function (done) { 1255 | var app = new Koa(); 1256 | var router = new Router(); 1257 | router 1258 | .param('user', function (id, ctx, next) { 1259 | ctx.user = { name: 'alex' }; 1260 | if (ctx.ranFirst) { 1261 | ctx.user.ordered = 'parameters'; 1262 | } 1263 | if (!id) return ctx.status = 404; 1264 | return next(); 1265 | }) 1266 | .param('first', function (id, ctx, next) { 1267 | ctx.ranFirst = true; 1268 | if (ctx.user) { 1269 | ctx.ranFirst = false; 1270 | } 1271 | if (!id) return ctx.status = 404; 1272 | return next(); 1273 | }) 1274 | .get('/:first/users/:user', function (ctx) { 1275 | ctx.body = ctx.user; 1276 | }); 1277 | 1278 | request(http.createServer( 1279 | app 1280 | .use(router.routes()) 1281 | .callback())) 1282 | .get('/first/users/3') 1283 | .expect(200) 1284 | .end(function (err, res) { 1285 | if (err) return done(err); 1286 | res.should.have.property('body'); 1287 | res.body.should.have.property('name', 'alex'); 1288 | res.body.should.have.property('ordered', 'parameters'); 1289 | done(); 1290 | }); 1291 | }); 1292 | 1293 | it('runs parameter middleware in order of URL appearance even when added in random order', function(done) { 1294 | var app = new Koa(); 1295 | var router = new Router(); 1296 | router 1297 | // intentional random order 1298 | .param('a', function (id, ctx, next) { 1299 | ctx.state.loaded = [ id ]; 1300 | return next(); 1301 | }) 1302 | .param('d', function (id, ctx, next) { 1303 | ctx.state.loaded.push(id); 1304 | return next(); 1305 | }) 1306 | .param('c', function (id, ctx, next) { 1307 | ctx.state.loaded.push(id); 1308 | return next(); 1309 | }) 1310 | .param('b', function (id, ctx, next) { 1311 | ctx.state.loaded.push(id); 1312 | return next(); 1313 | }) 1314 | .get('/:a/:b/:c/:d', function (ctx, next) { 1315 | ctx.body = ctx.state.loaded; 1316 | }); 1317 | 1318 | request(http.createServer( 1319 | app 1320 | .use(router.routes()) 1321 | .callback())) 1322 | .get('/1/2/3/4') 1323 | .expect(200) 1324 | .end(function(err, res) { 1325 | if (err) return done(err); 1326 | res.should.have.property('body'); 1327 | res.body.should.eql([ '1', '2', '3', '4' ]); 1328 | done(); 1329 | }); 1330 | }); 1331 | 1332 | it('runs parent parameter middleware for subrouter', function (done) { 1333 | var app = new Koa(); 1334 | var router = new Router(); 1335 | var subrouter = new Router(); 1336 | subrouter.get('/:cid', function (ctx) { 1337 | ctx.body = { 1338 | id: ctx.params.id, 1339 | cid: ctx.params.cid 1340 | }; 1341 | }); 1342 | router 1343 | .param('id', function (id, ctx, next) { 1344 | ctx.params.id = 'ran'; 1345 | if (!id) return ctx.status = 404; 1346 | return next(); 1347 | }) 1348 | .use('/:id/children', subrouter.routes()); 1349 | 1350 | request(http.createServer(app.use(router.routes()).callback())) 1351 | .get('/did-not-run/children/2') 1352 | .expect(200) 1353 | .end(function (err, res) { 1354 | if (err) return done(err); 1355 | res.should.have.property('body'); 1356 | res.body.should.have.property('id', 'ran'); 1357 | res.body.should.have.property('cid', '2'); 1358 | done(); 1359 | }); 1360 | }); 1361 | }); 1362 | 1363 | describe('Router#opts', function () { 1364 | it('responds with 200', function (done) { 1365 | var app = new Koa(); 1366 | var router = new Router({ 1367 | strict: true 1368 | }); 1369 | router.get('/info', function (ctx) { 1370 | ctx.body = 'hello'; 1371 | }); 1372 | request(http.createServer( 1373 | app 1374 | .use(router.routes()) 1375 | .callback())) 1376 | .get('/info') 1377 | .expect(200) 1378 | .end(function (err, res) { 1379 | if (err) return done(err); 1380 | res.text.should.equal('hello'); 1381 | done(); 1382 | }); 1383 | }); 1384 | 1385 | it('should allow setting a prefix', function (done) { 1386 | var app = new Koa(); 1387 | var routes = Router({ prefix: '/things/:thing_id' }); 1388 | 1389 | routes.get('/list', function (ctx) { 1390 | ctx.body = ctx.params; 1391 | }); 1392 | 1393 | app.use(routes.routes()); 1394 | 1395 | request(http.createServer(app.callback())) 1396 | .get('/things/1/list') 1397 | .expect(200) 1398 | .end(function (err, res) { 1399 | if (err) return done(err); 1400 | res.body.thing_id.should.equal('1'); 1401 | done(); 1402 | }); 1403 | }); 1404 | 1405 | it('responds with 404 when has a trailing slash', function (done) { 1406 | var app = new Koa(); 1407 | var router = new Router({ 1408 | strict: true 1409 | }); 1410 | router.get('/info', function (ctx) { 1411 | ctx.body = 'hello'; 1412 | }); 1413 | request(http.createServer( 1414 | app 1415 | .use(router.routes()) 1416 | .callback())) 1417 | .get('/info/') 1418 | .expect(404) 1419 | .end(function (err, res) { 1420 | if (err) return done(err); 1421 | done(); 1422 | }); 1423 | }); 1424 | }); 1425 | 1426 | describe('use middleware with opts', function () { 1427 | it('responds with 200', function (done) { 1428 | var app = new Koa(); 1429 | var router = new Router({ 1430 | strict: true 1431 | }); 1432 | router.get('/info', function (ctx) { 1433 | ctx.body = 'hello'; 1434 | }) 1435 | request(http.createServer( 1436 | app 1437 | .use(router.routes()) 1438 | .callback())) 1439 | .get('/info') 1440 | .expect(200) 1441 | .end(function (err, res) { 1442 | if (err) return done(err); 1443 | res.text.should.equal('hello'); 1444 | done(); 1445 | }); 1446 | }); 1447 | 1448 | it('responds with 404 when has a trailing slash', function (done) { 1449 | var app = new Koa(); 1450 | var router = new Router({ 1451 | strict: true 1452 | }); 1453 | router.get('/info', function (ctx) { 1454 | ctx.body = 'hello'; 1455 | }) 1456 | request(http.createServer( 1457 | app 1458 | .use(router.routes()) 1459 | .callback())) 1460 | .get('/info/') 1461 | .expect(404) 1462 | .end(function (err, res) { 1463 | if (err) return done(err); 1464 | done(); 1465 | }); 1466 | }); 1467 | }); 1468 | 1469 | describe('router.routes()', function () { 1470 | it('should return composed middleware', function (done) { 1471 | var app = new Koa(); 1472 | var router = new Router(); 1473 | var middlewareCount = 0; 1474 | var middlewareA = function (ctx, next) { 1475 | middlewareCount++; 1476 | return next(); 1477 | }; 1478 | var middlewareB = function (ctx, next) { 1479 | middlewareCount++; 1480 | return next(); 1481 | }; 1482 | 1483 | router.use(middlewareA, middlewareB); 1484 | router.get('/users/:id', function (ctx) { 1485 | should.exist(ctx.params.id); 1486 | ctx.body = { hello: 'world' }; 1487 | }); 1488 | 1489 | var routerMiddleware = router.routes(); 1490 | 1491 | expect(routerMiddleware).to.be.a('function'); 1492 | 1493 | request(http.createServer( 1494 | app 1495 | .use(routerMiddleware) 1496 | .callback())) 1497 | .get('/users/1') 1498 | .expect(200) 1499 | .end(function (err, res) { 1500 | if (err) return done(err); 1501 | expect(res.body).to.be.an('object'); 1502 | expect(res.body).to.have.property('hello', 'world'); 1503 | expect(middlewareCount).to.equal(2); 1504 | done(); 1505 | }); 1506 | }); 1507 | 1508 | it('places a `_matchedRoute` value on context', function(done) { 1509 | var app = new Koa(); 1510 | var router = new Router(); 1511 | var middleware = function (ctx, next) { 1512 | expect(ctx._matchedRoute).to.be('/users/:id') 1513 | return next(); 1514 | }; 1515 | 1516 | router.use(middleware); 1517 | router.get('/users/:id', function (ctx, next) { 1518 | expect(ctx._matchedRoute).to.be('/users/:id') 1519 | should.exist(ctx.params.id); 1520 | ctx.body = { hello: 'world' }; 1521 | }); 1522 | 1523 | var routerMiddleware = router.routes(); 1524 | 1525 | request(http.createServer( 1526 | app 1527 | .use(routerMiddleware) 1528 | .callback())) 1529 | .get('/users/1') 1530 | .expect(200) 1531 | .end(function(err, res) { 1532 | if (err) return done(err); 1533 | done(); 1534 | }); 1535 | }); 1536 | 1537 | it('places a `_matchedRouteName` value on the context for a named route', function(done) { 1538 | var app = new Koa(); 1539 | var router = new Router(); 1540 | 1541 | router.get('users#show', '/users/:id', function (ctx, next) { 1542 | expect(ctx._matchedRouteName).to.be('users#show') 1543 | ctx.status = 200 1544 | }); 1545 | 1546 | request(http.createServer(app.use(router.routes()).callback())) 1547 | .get('/users/1') 1548 | .expect(200) 1549 | .end(function(err, res) { 1550 | if (err) return done(err); 1551 | done(); 1552 | }); 1553 | }); 1554 | 1555 | it('does not place a `_matchedRouteName` value on the context for unnamed routes', function(done) { 1556 | var app = new Koa(); 1557 | var router = new Router(); 1558 | 1559 | router.get('/users/:id', function (ctx, next) { 1560 | expect(ctx._matchedRouteName).to.be(undefined) 1561 | ctx.status = 200 1562 | }); 1563 | 1564 | request(http.createServer(app.use(router.routes()).callback())) 1565 | .get('/users/1') 1566 | .expect(200) 1567 | .end(function(err, res) { 1568 | if (err) return done(err); 1569 | done(); 1570 | }); 1571 | }); 1572 | }); 1573 | 1574 | describe('If no HEAD method, default to GET', function () { 1575 | it('should default to GET', function (done) { 1576 | var app = new Koa(); 1577 | var router = new Router(); 1578 | router.get('/users/:id', function (ctx) { 1579 | should.exist(ctx.params.id); 1580 | ctx.body = 'hello'; 1581 | }); 1582 | request(http.createServer( 1583 | app 1584 | .use(router.routes()) 1585 | .callback())) 1586 | .head('/users/1') 1587 | .expect(200) 1588 | .end(function (err, res) { 1589 | if (err) return done(err); 1590 | expect(res.body).to.be.empty(); 1591 | done(); 1592 | }); 1593 | }); 1594 | 1595 | it('should work with middleware', function (done) { 1596 | var app = new Koa(); 1597 | var router = new Router(); 1598 | router.get('/users/:id', function (ctx) { 1599 | should.exist(ctx.params.id); 1600 | ctx.body = 'hello'; 1601 | }) 1602 | request(http.createServer( 1603 | app 1604 | .use(router.routes()) 1605 | .callback())) 1606 | .head('/users/1') 1607 | .expect(200) 1608 | .end(function (err, res) { 1609 | if (err) return done(err); 1610 | expect(res.body).to.be.empty(); 1611 | done(); 1612 | }); 1613 | }); 1614 | }); 1615 | 1616 | describe('Router#prefix', function () { 1617 | it('should set opts.prefix', function () { 1618 | var router = Router(); 1619 | expect(router.opts).to.not.have.key('prefix'); 1620 | router.prefix('/things/:thing_id'); 1621 | expect(router.opts.prefix).to.equal('/things/:thing_id'); 1622 | }); 1623 | 1624 | it('should prefix existing routes', function () { 1625 | var router = Router(); 1626 | router.get('/users/:id', function (ctx) { 1627 | ctx.body = 'test'; 1628 | }) 1629 | router.prefix('/things/:thing_id'); 1630 | var route = router.stack[0]; 1631 | expect(route.path).to.equal('/things/:thing_id/users/:id'); 1632 | expect(route.paramNames).to.have.length(2); 1633 | expect(route.paramNames[0]).to.have.property('name', 'thing_id'); 1634 | expect(route.paramNames[1]).to.have.property('name', 'id'); 1635 | }); 1636 | 1637 | describe('when used with .use(fn) - gh-247', function () { 1638 | it('does not set params.0 to the matched path', function (done) { 1639 | var app = new Koa(); 1640 | var router = new Router(); 1641 | 1642 | router.use(function(ctx, next) { 1643 | return next(); 1644 | }); 1645 | 1646 | router.get('/foo/:id', function(ctx) { 1647 | ctx.body = ctx.params; 1648 | }); 1649 | 1650 | router.prefix('/things'); 1651 | 1652 | app.use(router.routes()); 1653 | request(http.createServer(app.callback())) 1654 | .get('/things/foo/108') 1655 | .expect(200) 1656 | .end(function (err, res) { 1657 | if (err) return done(err); 1658 | 1659 | expect(res.body).to.have.property('id', '108'); 1660 | expect(res.body).to.not.have.property('0'); 1661 | done(); 1662 | }); 1663 | }); 1664 | }); 1665 | 1666 | describe('with trailing slash', testPrefix('/admin/')); 1667 | describe('without trailing slash', testPrefix('/admin')); 1668 | 1669 | function testPrefix(prefix) { 1670 | return function () { 1671 | var server; 1672 | var middlewareCount = 0; 1673 | 1674 | before(function () { 1675 | var app = new Koa(); 1676 | var router = Router(); 1677 | 1678 | router.use(function (ctx, next) { 1679 | middlewareCount++; 1680 | ctx.thing = 'worked'; 1681 | return next(); 1682 | }); 1683 | 1684 | router.get('/', function (ctx) { 1685 | middlewareCount++; 1686 | ctx.body = { name: ctx.thing }; 1687 | }); 1688 | 1689 | router.prefix(prefix); 1690 | server = http.createServer(app.use(router.routes()).callback()); 1691 | }); 1692 | 1693 | after(function () { 1694 | server.close(); 1695 | }); 1696 | 1697 | beforeEach(function () { 1698 | middlewareCount = 0; 1699 | }); 1700 | 1701 | it('should support root level router middleware', function (done) { 1702 | request(server) 1703 | .get(prefix) 1704 | .expect(200) 1705 | .end(function (err, res) { 1706 | if (err) return done(err); 1707 | expect(middlewareCount).to.equal(2); 1708 | expect(res.body).to.be.an('object'); 1709 | expect(res.body).to.have.property('name', 'worked'); 1710 | done(); 1711 | }); 1712 | }); 1713 | 1714 | it('should support requests with a trailing path slash', function (done) { 1715 | request(server) 1716 | .get('/admin/') 1717 | .expect(200) 1718 | .end(function (err, res) { 1719 | if (err) return done(err); 1720 | expect(middlewareCount).to.equal(2); 1721 | expect(res.body).to.be.an('object'); 1722 | expect(res.body).to.have.property('name', 'worked'); 1723 | done(); 1724 | }); 1725 | }); 1726 | 1727 | it('should support requests without a trailing path slash', function (done) { 1728 | request(server) 1729 | .get('/admin') 1730 | .expect(200) 1731 | .end(function (err, res) { 1732 | if (err) return done(err); 1733 | expect(middlewareCount).to.equal(2); 1734 | expect(res.body).to.be.an('object'); 1735 | expect(res.body).to.have.property('name', 'worked'); 1736 | done(); 1737 | }); 1738 | }); 1739 | } 1740 | } 1741 | }); 1742 | 1743 | describe('Static Router#url()', function () { 1744 | it('generates route URL', function () { 1745 | var url = Router.url('/:category/:title', { category: 'programming', title: 'how-to-node' }); 1746 | url.should.equal('/programming/how-to-node'); 1747 | }); 1748 | 1749 | it('escapes using encodeURIComponent()', function () { 1750 | var url = Router.url('/:category/:title', { category: 'programming', title: 'how to node' }); 1751 | url.should.equal('/programming/how%20to%20node'); 1752 | }); 1753 | 1754 | it('generates route URL with params and query params', function(done) { 1755 | var url = Router.url('/books/:category/:id', 'programming', 4, { 1756 | query: { page: 3, limit: 10 } 1757 | }); 1758 | url.should.equal('/books/programming/4?page=3&limit=10'); 1759 | var url = Router.url('/books/:category/:id', 1760 | { category: 'programming', id: 4 }, 1761 | { query: { page: 3, limit: 10 }} 1762 | ); 1763 | url.should.equal('/books/programming/4?page=3&limit=10'); 1764 | var url = Router.url('/books/:category/:id', 1765 | { category: 'programming', id: 4 }, 1766 | { query: 'page=3&limit=10' } 1767 | ); 1768 | url.should.equal('/books/programming/4?page=3&limit=10'); 1769 | done(); 1770 | }); 1771 | 1772 | it('generates router URL without params and with with query params', function(done) { 1773 | var url = Router.url('/category', { 1774 | query: { page: 3, limit: 10 } 1775 | }); 1776 | url.should.equal('/category?page=3&limit=10'); 1777 | done(); 1778 | }); 1779 | }); 1780 | }); 1781 | --------------------------------------------------------------------------------