├── .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 | [](https://npmjs.org/package/koa-router) [](https://npmjs.org/package/koa-router) [](http://nodejs.org/download/) [](http://travis-ci.org/alexmingoia/koa-router) [](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 | [](https://npmjs.org/package/koa-router) [](https://npmjs.org/package/koa-router) [](http://nodejs.org/download/) [](http://travis-ci.org/alexmingoia/koa-router) [](https://www.gratipay.com/alexmingoia/) [](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 |
--------------------------------------------------------------------------------