├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── basic.js ├── before.js └── debug.js ├── index.js ├── package-lock.json ├── package.json ├── src └── router.js └── test.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional eslint cache 38 | .eslintcache 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # Output of 'npm pack' 44 | *.tgz 45 | 46 | # Yarn Integrity file 47 | .yarn-integrity 48 | 49 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Optional npm cache directory 28 | .npm 29 | 30 | # Optional eslint cache 31 | .eslintcache 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # Output of 'npm pack' 37 | *.tgz 38 | 39 | # Yarn Integrity file 40 | .yarn-integrity 41 | 42 | .gitattributes 43 | example.js 44 | before.js 45 | .travis.yml 46 | test.js 47 | 48 | .idea 49 | examples -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | after_success: 5 | - './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nick Duncan 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micro-http-router 2 | [![Build Status](https://travis-ci.org/protocol114/micro-http-router.svg?branch=master)](https://travis-ci.org/protocol114/micro-http-router) [![Coverage Status](https://coveralls.io/repos/github/protocol114/micro-http-router/badge.svg?branch=master)](https://coveralls.io/github/protocol114/micro-http-router?branch=master) 3 | 4 | micro-http-router is a simple, Express-like router for [micro](https://github.com/zeit/micro) that uses a radix tree via [radix-router](https://github.com/charlieduong94/radix-router). It supports most, if not all, of the HTTP verbs, and tries to be as lightweight and quick as possible. 5 | 6 | ## Installation 7 | micro-http-router is an npm package and you should have the latest Node.js and npm installed. 8 | 9 | ```bash 10 | > npm i --save micro-http-router 11 | ``` 12 | 13 | https://www.npmjs.com/package/micro-http-router 14 | 15 | ## Usage 16 | 17 | ```javascript 18 | const micro = require('micro'); 19 | const Router = require('micro-http-router'); 20 | 21 | // Initialize the router 22 | const router = new Router(); 23 | 24 | // Define a basic GET request 25 | router.route({ 26 | path: '/', 27 | method: 'GET', 28 | handler: (req, res) => { 29 | return 'Hello, world'; 30 | } 31 | }); 32 | 33 | // Define a basic GET request with a middleware function 34 | router.route({ 35 | path: '/', 36 | method: 'GET', 37 | before: (req, res) => { 38 | req.user = {}; 39 | req.user.name = 'John Doe'; 40 | }, 41 | handler: (req, res) => { 42 | return `Hello, ${ req.user.name }`; 43 | } 44 | }); 45 | 46 | // Express-like routing helpers 47 | router.get('/', (req, res) => { 48 | return 'Hello, world'; 49 | }); 50 | 51 | // Async body parsing 52 | router.post('/', async (req, res) => { 53 | const body = await micro.json(req); 54 | return body; 55 | }); 56 | 57 | // Any number of route parameters are supported 58 | // Access via the req.params array 59 | router.get('/:first/:second', (req, res) => { 60 | const first = req.params.first; 61 | const second = req.params.second; 62 | return `Your first parameter is ${ first } and your second is ${ second }.`; 63 | }); 64 | 65 | // Start micro and listen 66 | const server = micro((req, res) => router.handle(req, res)); 67 | const port = 3000; 68 | server.listen(port); 69 | console.log(`micro is listening on port: ${ port }`); 70 | ``` 71 | 72 | ## Changelog (1.3.0 => 1.5.1) 73 | 74 | ##### New in 1.5.1 75 | Fixed some potential issues with the unrouting functionality. Upgraded dependencies to remove potential vulnerabilities. 76 | 77 | ##### New in 1.5.0 78 | Added support for query parameters using WHATWG URL API. The WHATWG URL `.searchParams` object gets transplanted onto the `request` object as `.searchParams` as well for a familiar API for retrieving and manipulating those query parameters. 79 | 80 | Also added `unroute(path, method)` function to the router that can be called to delete route handlers, if necessary. It can be used in this manner: 81 | 82 | ```javascript 83 | // Configure a basic get route 84 | router.get('/', (req, res) => { 85 | ... 86 | }); 87 | 88 | // don't want the route? call router.unroute(path, method)! 89 | router.unroute('/', 'GET'); 90 | 91 | // Now subsequent calls to that route and method will fail, as it no longer exists. 👍 92 | ``` 93 | 94 | You can also remove all configured routes by calling `router.unrouteAll()`, and you can see configured routes and what methods are configured by accessing `router.routes`. 95 | 96 | ```javascript 97 | const router = new Router(); 98 | 99 | // Configure the routes 100 | router.get('/foo', (req, res) => { 101 | ... 102 | }); 103 | router.post('/foo', (req, res) => { 104 | ... 105 | }); 106 | router.get('/bar', (req, res) => { 107 | ... 108 | }); 109 | 110 | console.log(router.routes); 111 | // Returns: 112 | { '/foo': [ 'GET', 'POST' ], '/bar': [ 'GET' ] } 113 | ``` 114 | 115 | ##### New in 1.3.0 116 | Updated dependencies, including radix-router. The only major change is that in place of a request parameter array, the parameters are added to the `req.params` object in a named fashion. If you define a route as `'/:id'`, instead of using `req.params[0]` to get the passed in ID, you can simply use `req.params.id`. Additionally, merged in [PR #3](https://github.com/protocol114/micro-http-router/pull/3) which adds a full stack trace to the error output when `debug` is set to `true`. 117 | 118 | ##### New in 1.2.0 119 | Add `debug: true` to the router options object to enable debug logging. Any thrown errors will have their error messages returned as the response body. Useful when developing with micro-http-router and you are unexpectedly receiving 500 Internal Server Errors. 120 | 121 | ##### New in 1.1.0 122 | Added `before` option to route options. This serves as a single layer of middleware allowing you to do the following: 123 | 124 | ```javascript 125 | const micro = require('micro'); 126 | const Router = require('micro-http-router'); 127 | 128 | const beforeHandler = function (req, res) { 129 | req.user = {}; 130 | req.user.name = 'John Doe'; 131 | }; 132 | 133 | // Initialize the router 134 | const router = new Router(); 135 | 136 | // Define a basic GET request with a middleware function 137 | router.route({ 138 | path: '/', 139 | method: 'GET', 140 | before: beforeHandler, 141 | handler: (req, res) => { 142 | return `Hello, ${ req.user.name }`; 143 | } 144 | }); 145 | 146 | // or define it with your get shorthand 147 | router.get('/', beforeHandler, (req, res) => { 148 | return `Hello, ${ req.user.name }`; 149 | }); 150 | 151 | // Start micro and listen 152 | const server = micro((req, res) => router.handle(req, res)); 153 | const port = 3000; 154 | server.listen(port); 155 | console.log(`micro is listening on port: ${ port }`); 156 | ``` 157 | 158 | ## Tests 159 | `micro-http-router` comes with a few [AVA](https://github.com/avajs/ava) tests to help ensure correct routing behavior. 160 | 161 | To execute the tests yourself, simply run: 162 | 163 | `npm test` 164 | 165 | ## License 166 | MIT License 167 | 168 | Copyright (c) 2017 Nick Duncan 169 | 170 | Permission is hereby granted, free of charge, to any person obtaining a copy 171 | of this software and associated documentation files (the "Software"), to deal 172 | in the Software without restriction, including without limitation the rights 173 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 174 | copies of the Software, and to permit persons to whom the Software is 175 | furnished to do so, subject to the following conditions: 176 | 177 | The above copyright notice and this permission notice shall be included in all 178 | copies or substantial portions of the Software. 179 | 180 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 181 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 182 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 183 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 184 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 185 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 186 | SOFTWARE. -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | const micro = require('micro'); 2 | const Router = require('../'); 3 | 4 | // Initialize the router 5 | const router = new Router(); 6 | 7 | // Define a basic GET request 8 | router.route({ 9 | path: '/', 10 | method: 'GET', 11 | handler: (req, res) => { 12 | return 'Hello, world'; 13 | } 14 | }); 15 | 16 | // Express-like routing helpers 17 | router.get('/', (req, res) => { 18 | return 'Hello, world'; 19 | }); 20 | 21 | // Async body parsing 22 | router.post('/', async (req, res) => { 23 | const body = await micro.json(req); 24 | return body; 25 | }); 26 | 27 | // Any number of route parameters are supported 28 | // Access via the req.params array 29 | router.get('/:first/:second', (req, res) => { 30 | const first = req.params[0]; 31 | const second = req.params[1]; 32 | return `Your first parameter is ${ first } and your second is ${ second }.`; 33 | }); 34 | 35 | // Start micro and listen 36 | const server = micro((req, res) => router.handle(req, res)); 37 | const port = 3000; 38 | server.listen(port); 39 | console.log(`micro is listening on port: ${ port }`); -------------------------------------------------------------------------------- /examples/before.js: -------------------------------------------------------------------------------- 1 | const micro = require('micro'); 2 | const Router = require('../'); 3 | 4 | // Initialize the router 5 | const router = new Router(); 6 | 7 | const beforeHandler = function (req, res) { 8 | req.user = {}; 9 | req.user.name = 'John Doe'; 10 | } 11 | 12 | // Define a basic GET request with a middleware function 13 | router.route({ 14 | path: '/', 15 | method: 'GET', 16 | before: beforeHandler, 17 | handler: (req, res) => { 18 | return `Hello, ${ req.user.name }`; 19 | } 20 | }); 21 | 22 | // or define it with your get shorthand 23 | router.get('/', beforeHandler, (req, res) => { 24 | return `Hello, ${ req.user.name }`; 25 | }); 26 | 27 | // Start micro and listen 28 | const server = micro((req, res) => router.handle(req, res)); 29 | const port = 3000; 30 | server.listen(port); 31 | console.log(`micro is listening on port: ${ port }`); -------------------------------------------------------------------------------- /examples/debug.js: -------------------------------------------------------------------------------- 1 | const micro = require('micro'); 2 | const Router = require('../'); 3 | 4 | // Initialize the router 5 | const router = new Router({ 6 | debug: true 7 | }); 8 | 9 | router.get('/', (req, res) => { 10 | throw new Error('A bad thing happened, sorry.'); 11 | }); 12 | 13 | // Start micro and listen 14 | const server = micro((req, res) => router.handle(req, res)); 15 | const port = 3000; 16 | server.listen(port); 17 | console.log(`micro is listening on port: ${ port }`); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Router = require('./src/router'); 2 | 3 | module.exports = exports = Router; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micro-http-router", 3 | "version": "1.5.1", 4 | "description": "A tiny, simple, fast and easy router package for Micro.", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "micro-dev", 8 | "test": "nyc ava" 9 | }, 10 | "author": "Nick Duncan ", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/protocol114/micro-http-router" 14 | }, 15 | "license": "MIT", 16 | "devDependencies": { 17 | "ava": "^1.4.1", 18 | "coveralls": "^3.0.3", 19 | "micro-dev": "^3.0.0", 20 | "nyc": "^14.1.1", 21 | "request": "^2.88.0", 22 | "request-promise": "^4.2.4", 23 | "sleep-promise": "^2.0.0", 24 | "test-listen": "^1.1.0" 25 | }, 26 | "dependencies": { 27 | "assert": "^1.5.0", 28 | "micro": "^9.3.4", 29 | "radix-router": "^3.0.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {send, createError} = require('micro'); 4 | const RadixRouter = require('radix-router'); 5 | const assert = require('assert'); 6 | 7 | const routerSymbol = Symbol(); 8 | 9 | const routeOptionsHelper = function (path, method, fn1, fn2) { 10 | let options = { 11 | path: path, 12 | method: method 13 | }; 14 | 15 | assert(fn1, 'You must provide a valid function as the second parameter.'); 16 | 17 | if (fn2) { 18 | options.before = fn1; 19 | options.handler = fn2; 20 | } else { 21 | options.handler = fn1; 22 | } 23 | 24 | return options; 25 | }; 26 | 27 | module.exports = exports = class Router { 28 | constructor(options) { 29 | // Add new instance of RadixRouter 30 | this[routerSymbol] = new RadixRouter({strict: options && options.strict}); 31 | 32 | this.debug = options && options.debug; 33 | 34 | this.routes = {}; 35 | } 36 | 37 | /** 38 | * Adds a route handler to the router. 39 | * @param {object} options The route information and handler 40 | */ 41 | route(options) { 42 | assert(options, 'You must provide a valid route options object.'); 43 | assert(options.path, 'You must provide a valid path.'); 44 | assert(options.method, 'You must provide a valid route handler function.'); 45 | assert(options.handler, 'You must provide a valid route handler function.'); 46 | 47 | // Sanitize options 48 | options.path = options.path.toLowerCase(); 49 | options.method = options.method.toUpperCase(); 50 | 51 | const existingRoute = this[routerSymbol].lookup(options.path); 52 | let route = {}; 53 | if (existingRoute) { 54 | route = existingRoute; 55 | } else { 56 | route = { 57 | path: options.path, 58 | methods: {} 59 | }; 60 | 61 | this[routerSymbol].insert(route); 62 | } 63 | 64 | if (options.path in this.routes) { 65 | const existingMethodIdx = this.routes[options.path].findIndex((method) => method === options.method); 66 | 67 | if (existingMethodIdx === -1) { 68 | this.routes[options.path].push(options.method); 69 | this.routes[options.path].sort(); 70 | } 71 | } else { 72 | this.routes[options.path] = [options.method]; 73 | } 74 | 75 | route.methods[options.method] = { 76 | handler: options.handler 77 | }; 78 | 79 | if (options.before) { 80 | assert(typeof options.before === 'function'); 81 | 82 | route.methods[options.method].before = options.before; 83 | } 84 | 85 | return this; 86 | } 87 | 88 | /** 89 | * Deregisters a route with the specified path and method. 90 | * @param {string} path The desired path to deregister. 91 | * @param {string} method The desired method to deregister. 92 | */ 93 | unroute(path, method) { 94 | assert(path, 'You must provide a valid path.'); 95 | assert(method, 'You must provide a valid route handler function.'); 96 | 97 | method = method.toUpperCase(); 98 | 99 | const existingRoute = this[routerSymbol].lookup(path); 100 | let route = {}; 101 | if (existingRoute) { 102 | route = existingRoute; 103 | } else { 104 | throw new Error(`Route with path '${path}' not found to unroute.`); 105 | } 106 | 107 | const existingMethodIdx = this.routes[path].findIndex((pathMethod) => pathMethod === method); 108 | if (route.methods[method] && existingMethodIdx !== -1) { 109 | delete route.methods[method]; 110 | this.routes[path].splice(existingMethodIdx, 1); 111 | 112 | if (this.routes[path].length === 0) { 113 | delete this.routes[path]; 114 | this[routerSymbol].remove(path); 115 | } 116 | } else { 117 | throw new Error(`Route ${method} "${path}" not found to unroute.`); 118 | } 119 | 120 | return this; 121 | } 122 | 123 | /** 124 | * Deregisters all currently defined routes in the router. 125 | */ 126 | unrouteAll() { 127 | for (let path in this.routes) { 128 | this.routes[path].forEach((method) => { 129 | this.unroute(path, method); 130 | }); 131 | } 132 | } 133 | 134 | /** 135 | * Chains a HTTP "GET" route handler to the router object. 136 | * @param {String} path 137 | * @param {Function} fn1 138 | * @param {Function} fn2 139 | */ 140 | get(path, fn1, fn2) { 141 | return this.route(routeOptionsHelper(path, 'GET', fn1, fn2)); 142 | } 143 | 144 | /** 145 | * Chains a HTTP "POST" route handler to the router object. 146 | * @param {String} path 147 | * @param {Function} fn1 148 | * @param {Function} fn2 149 | */ 150 | post(path, fn1, fn2) { 151 | return this.route(routeOptionsHelper(path, 'POST', fn1, fn2)); 152 | } 153 | 154 | /** 155 | * Chains a HTTP "PUT" route handler to the router object. 156 | * @param {String} path 157 | * @param {Function} fn1 158 | * @param {Function} fn2 159 | */ 160 | put(path, fn1, fn2) { 161 | return this.route(routeOptionsHelper(path, 'PUT', fn1, fn2)); 162 | } 163 | 164 | /** 165 | * Chains a HTTP "DELETE" route handler to the router object. 166 | * @param {String} path 167 | * @param {Function} fn1 168 | * @param {Function} fn2 169 | */ 170 | delete(path, fn1, fn2) { 171 | return this.route(routeOptionsHelper(path, 'DELETE', fn1, fn2)); 172 | } 173 | 174 | /** 175 | * Chains a HTTP "OPTIONS" route handler to the router object. 176 | * @param {String} path 177 | * @param {Function} fn1 178 | * @param {Function} fn2 179 | */ 180 | options(path, fn1, fn2) { 181 | return this.route(routeOptionsHelper(path, 'OPTIONS', fn1, fn2)); 182 | } 183 | 184 | /** 185 | * Chains a HTTP "TRACE" route handler to the router object. 186 | * @param {String} path 187 | * @param {Function} fn1 188 | * @param {Function} fn2 189 | */ 190 | trace(path, fn1, fn2) { 191 | return this.route(routeOptionsHelper(path, 'TRACE', fn1, fn2)); 192 | } 193 | 194 | /** 195 | * Chains a HTTP "PATCH" route handler to the router object. 196 | * @param {String} path 197 | * @param {Function} fn1 198 | * @param {Function} fn2 199 | */ 200 | patch(path, fn1, fn2) { 201 | return this.route(routeOptionsHelper(path, 'PATCH', fn1, fn2)); 202 | } 203 | 204 | /** 205 | * Handles HTTP traffic according to the router. 206 | * @param {object} req http.incomingMessage 207 | * @param {object} res http.serverResponse 208 | */ 209 | async handle(req, res) { 210 | const reqURL = new URL(req.url, 'http://localhost/'); 211 | const route = this[routerSymbol].lookup(reqURL.pathname); 212 | 213 | req.method = req.method.toUpperCase(); 214 | 215 | if (route && req.method in route.methods) { 216 | try { 217 | const methodObj = route.methods[req.method]; 218 | // Set the params if we have any 219 | if (route.params) req.params = route.params; 220 | 221 | // set query parameters as well 222 | req.searchParams = reqURL.searchParams; 223 | 224 | // Run the before function if one is configured 225 | if (methodObj.before) methodObj.before(req, res); 226 | 227 | // Finally, handle the result 228 | const result = await methodObj.handler(req, res); 229 | send(res, 200, result); 230 | } catch (e) { 231 | let data = null; 232 | if (this.debug) 233 | data = `${e.name}: ${e.message}\n${e.stack}`; 234 | send(res, 500, data); 235 | } 236 | } else { 237 | throw createError(404, 'Route not found'); 238 | } 239 | } 240 | }; 241 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import micro from "micro"; 3 | import listen from "test-listen"; 4 | import request from "request-promise"; 5 | import sleep from "sleep-promise"; 6 | import Router from "./"; 7 | 8 | test('simple request handled successfully', async t => { 9 | // Create new instance of micro-http-router 10 | const router = new Router(); 11 | 12 | // Configure the routes 13 | router.get('/', (req, res) => { 14 | micro.send(res, 200, { 15 | success: true 16 | }); 17 | }); 18 | 19 | // Create the service 20 | const service = micro((req, res) => router.handle(req, res)); 21 | 22 | // Listen to the service and make the request to route '/' 23 | const url = await listen(service); 24 | const response = await request(url); 25 | 26 | // Perform the test check 27 | t.deepEqual(JSON.parse(response).success, true); 28 | 29 | // Close the service 30 | service.close(); 31 | }); 32 | 33 | test('all HTTP methods are successful', async t => { 34 | // Create new instance of micro-http-router 35 | const router = new Router(); 36 | 37 | const methods = [ 38 | 'get', 39 | 'post', 40 | 'put', 41 | 'delete', 42 | 'options', 43 | 'trace', 44 | 'patch' 45 | ]; 46 | 47 | router.get('/', (req, res) => { 48 | micro.send(res, 200, 'get'); 49 | }); 50 | router.post('/', (req, res) => { 51 | micro.send(res, 200, 'post'); 52 | }); 53 | router.put('/', (req, res) => { 54 | micro.send(res, 200, 'put'); 55 | }); 56 | router.delete('/', (req, res) => { 57 | micro.send(res, 200, 'delete'); 58 | }); 59 | router.options('/', (req, res) => { 60 | micro.send(res, 200, 'options'); 61 | }); 62 | router.trace('/', (req, res) => { 63 | micro.send(res, 200, 'trace'); 64 | }); 65 | router.patch('/', (req, res) => { 66 | micro.send(res, 200, 'patch'); 67 | }); 68 | 69 | // Create the service 70 | const service = micro((req, res) => router.handle(req, res)); 71 | 72 | // Listen to the service and make the request to route '/' 73 | const url = await listen(service); 74 | 75 | for (const method of methods) { 76 | const response = await request({ 77 | uri: url, 78 | method: method 79 | }); 80 | 81 | // Perform the test check 82 | t.deepEqual(response, method); 83 | 84 | // wait 50 ms to allow requests to complete 85 | await sleep(50); 86 | } 87 | 88 | // Close the service 89 | service.close(); 90 | }); 91 | 92 | test('strict mode works correctly', async t => { 93 | // Create new instance of micro-http-router 94 | const router = new Router({strict: true}); 95 | 96 | // Configure the routes 97 | router.get('/strict', (req, res) => { 98 | micro.send(res, 200, { 99 | success: true 100 | }); 101 | }); 102 | 103 | // Create the service 104 | const service = micro((req, res) => router.handle(req, res)); 105 | 106 | // Listen to the service and make the request to route '/' 107 | const url = await listen(service); 108 | let response = await request(`${ url }/strict`); 109 | 110 | // Perform the test check 111 | t.deepEqual(JSON.parse(response).success, true); 112 | 113 | try { 114 | response = await request(`${ url }/strict/`); 115 | } catch (e) { 116 | if (e && e.statusCode && e.statusCode === 404) { 117 | t.pass(); 118 | } else { 119 | t.fail(); 120 | } 121 | } 122 | 123 | // Close the service 124 | service.close(); 125 | }); 126 | 127 | test('error in handler logic handled successfully', async t => { 128 | // Create new instance of micro-http-router 129 | const router = new Router(); 130 | 131 | // Configure the routes 132 | router.get('/', (req, res) => { 133 | throw new Error(); 134 | }); 135 | 136 | // Create the service 137 | const service = micro((req, res) => router.handle(req, res)); 138 | 139 | // Listen to the service and make the request to route '/' 140 | const url = await listen(service); 141 | 142 | // Perform the test check 143 | // In try/catch block since the request promise will fail 144 | try { 145 | const response = await request(url); 146 | } catch (e) { 147 | if (e && e.statusCode && e.statusCode === 500) { 148 | t.pass(); 149 | } else { 150 | t.fail(); 151 | } 152 | } 153 | 154 | // Close the service 155 | service.close(); 156 | }); 157 | 158 | test('error 404 when no route found', async t => { 159 | // Create new instance of micro-http-router 160 | const router = new Router(); 161 | 162 | // Configure the routes 163 | router.get('/foo', (req, res) => { 164 | micro.send(res, 200, { 165 | success: true 166 | }); 167 | }); 168 | 169 | // Create the service 170 | const service = micro((req, res) => router.handle(req, res)); 171 | 172 | // Listen to the service and make the request to route '/' 173 | const url = await listen(service); 174 | 175 | // Perform the test check 176 | try { 177 | const response = await request(url); 178 | } catch (e) { 179 | if (e && e.statusCode && e.statusCode === 404) { 180 | t.pass(); 181 | } else { 182 | t.fail(); 183 | } 184 | } 185 | 186 | // Close the service 187 | service.close(); 188 | }); 189 | 190 | test('correct route selected with multiple routes defined', async t => { 191 | // Create new instance of micro-http-router 192 | const router = new Router(); 193 | 194 | // Configure the routes 195 | router.get('/foo', (req, res) => { 196 | micro.send(res, 200, 'foo'); 197 | }); 198 | router.get('/bar', (req, res) => { 199 | micro.send(res, 200, 'bar'); 200 | }); 201 | router.get('/baz', (req, res) => { 202 | micro.send(res, 200, 'baz'); 203 | }); 204 | router.get('/qux', (req, res) => { 205 | micro.send(res, 200, 'qux'); 206 | }); 207 | 208 | // Create the service 209 | const service = micro((req, res) => router.handle(req, res)); 210 | 211 | // Listen to the service and make the request to route '/' 212 | const url = await listen(service); 213 | const response = await request(`${ url }/bar`); 214 | 215 | // Perform the test check 216 | t.notDeepEqual(response, 'foo'); 217 | t.notDeepEqual(response, 'baz'); 218 | t.notDeepEqual(response, 'qux'); 219 | t.deepEqual(response, 'bar'); 220 | 221 | // Close the service 222 | service.close(); 223 | }); 224 | 225 | test('correct method selected for routes with same path', async t => { 226 | // Create new instance of micro-http-router 227 | const router = new Router(); 228 | 229 | // Configure the routes 230 | router.get('/', (req, res) => { 231 | micro.send(res, 200, 'foo'); 232 | }); 233 | router.post('/', (req, res) => { 234 | micro.send(res, 200, 'bar'); 235 | }); 236 | router.put('/', (req, res) => { 237 | micro.send(res, 200, 'baz'); 238 | }); 239 | router.delete('/', (req, res) => { 240 | micro.send(res, 200, 'qux'); 241 | }); 242 | 243 | // Create the service 244 | const service = micro((req, res) => router.handle(req, res)); 245 | 246 | // Listen to the service and make the request to route '/' 247 | const url = await listen(service); 248 | const response = await request({ 249 | method: 'POST', 250 | uri: url 251 | }); 252 | 253 | // Perform the test check 254 | t.notDeepEqual(response, 'foo'); 255 | t.notDeepEqual(response, 'baz'); 256 | t.notDeepEqual(response, 'qux'); 257 | t.deepEqual(response, 'bar'); 258 | 259 | // Close the service 260 | service.close(); 261 | }); 262 | 263 | test('route handlers can be overridden correctly', async t => { 264 | // Create new instance of micro-http-router 265 | const router = new Router(); 266 | 267 | // Configure the routes 268 | router.get('/', (req, res) => { 269 | micro.send(res, 200, 'foo'); 270 | }); 271 | router.get('/', (req, res) => { 272 | micro.send(res, 200, 'bar'); 273 | }); 274 | router.get('/', (req, res) => { 275 | micro.send(res, 200, 'baz'); 276 | }); 277 | 278 | // Create the service 279 | const service = micro((req, res) => router.handle(req, res)); 280 | 281 | // Listen to the service and make the request to route '/' 282 | const url = await listen(service); 283 | const response = await request(url); 284 | 285 | // Perform the test check 286 | t.notDeepEqual(response, 'foo'); 287 | t.notDeepEqual(response, 'bar'); 288 | t.deepEqual(response, 'baz'); 289 | 290 | // Close the service 291 | service.close(); 292 | }); 293 | 294 | test('request parameters are mapped correctly', async t => { 295 | // Create new instance of micro-http-router 296 | const router = new Router(); 297 | 298 | // Configure the route 299 | router.get('/:zero/:one/:two/:three', (req, res) => { 300 | micro.send(res, 200, [ 301 | req.params.zero, 302 | req.params.one, 303 | req.params.two, 304 | req.params.three 305 | ]); 306 | }); 307 | 308 | // Create the service 309 | const service = micro((req, res) => router.handle(req, res)); 310 | 311 | // Listen to the service and make the request 312 | const url = await listen(service); 313 | 314 | const response = await request(`${ url }/zero/one/two/three`); 315 | t.deepEqual(JSON.parse(response)[0], 'zero'); 316 | t.deepEqual(JSON.parse(response)[1], 'one'); 317 | t.deepEqual(JSON.parse(response)[2], 'two'); 318 | t.deepEqual(JSON.parse(response)[3], 'three'); 319 | 320 | // Close the service 321 | service.close(); 322 | }); 323 | 324 | test('request parameters load test', async t => { 325 | // Create new instance of micro-http-router 326 | const router = new Router(); 327 | 328 | // Configure the route 329 | router.get('/:number', (req, res) => { 330 | micro.send(res, 200, req.params.number); 331 | }); 332 | 333 | // Create the service 334 | const service = micro((req, res) => router.handle(req, res)); 335 | 336 | // Listen to the service and make the request to route '/:number' 337 | const url = await listen(service); 338 | 339 | for (let i = 0; i < 100; i++) { 340 | const response = await request(`${ url }/${ i }`); 341 | t.deepEqual(response, i.toString()); 342 | } 343 | 344 | // Close the service 345 | service.close(); 346 | }); 347 | 348 | test('before request works correctly', async t => { 349 | // Create new instance of micro-http-router 350 | const router = new Router(); 351 | 352 | // Configure the routes 353 | router.route({ 354 | path: '/', 355 | method: 'GET', 356 | before: (req, res) => { 357 | req.testdata = 'foobar'; 358 | }, 359 | handler: (req, res) => { 360 | return req.testdata; 361 | } 362 | }); 363 | 364 | // Create the service 365 | const service = micro((req, res) => router.handle(req, res)); 366 | 367 | // Listen to the service and make the request to route '/' 368 | const url = await listen(service); 369 | const response = await request(url); 370 | 371 | // Perform the test check 372 | t.deepEqual(response, 'foobar'); 373 | 374 | // Close the service 375 | service.close(); 376 | }); 377 | 378 | test('before request using http method shorthand works correctly', async t => { 379 | // Create new instance of micro-http-router 380 | const router = new Router(); 381 | 382 | const beforeHandler = (req, res) => { 383 | req.testdata = 'foobar'; 384 | } 385 | 386 | // Configure the routes 387 | router.get('/', beforeHandler, (req, res) => { 388 | return req.testdata; 389 | }); 390 | 391 | // Create the service 392 | const service = micro((req, res) => router.handle(req, res)); 393 | 394 | // Listen to the service and make the request to route '/' 395 | const url = await listen(service); 396 | const response = await request(url); 397 | 398 | // Perform the test check 399 | t.deepEqual(response, 'foobar'); 400 | 401 | // Close the service 402 | service.close(); 403 | }); 404 | 405 | test('debug error logging is successful', async t => { 406 | // Create new instance of micro-http-router 407 | const router = new Router({ 408 | debug: true 409 | }); 410 | const errorMessage = 'test error'; 411 | 412 | // Configure the routes 413 | router.get('/', (req, res) => { 414 | throw new Error(errorMessage); 415 | }); 416 | 417 | // Create the service 418 | const service = micro((req, res) => router.handle(req, res)); 419 | 420 | // Listen to the service and make the request to route '/' 421 | const url = await listen(service); 422 | 423 | try { 424 | await request(url); 425 | } catch (e) { 426 | if (e && e.statusCode && e.statusCode === 500 && e.message.indexOf(errorMessage) !== -1) { 427 | t.pass(); 428 | } else { 429 | t.fail(); 430 | } 431 | } 432 | 433 | // Close the service 434 | service.close(); 435 | }); 436 | 437 | test('route with query params works', async t => { 438 | // Create new instance of micro-http-router 439 | const router = new Router(); 440 | 441 | // Configure the routes 442 | router.get('/', (req, res) => { 443 | micro.send(res, 200, { 444 | hello: req.searchParams.get('hello') 445 | }); 446 | }); 447 | 448 | // Create the service 449 | const service = micro((req, res) => router.handle(req, res)); 450 | 451 | // Listen to the service and make the request to route '/' 452 | const url = await listen(service); 453 | const response = await request(`${url}/?hello=world` ); 454 | 455 | // Perform the test check 456 | t.deepEqual(JSON.parse(response).hello, 'world'); 457 | 458 | // Close the service 459 | service.close(); 460 | }); 461 | 462 | test('route can be deregistered successfully', async t => { 463 | // Create new instance of micro-http-router 464 | const router = new Router(); 465 | 466 | // Configure the routes 467 | router.get('/', (req, res) => { 468 | micro.send(res, 200, { 469 | success: true 470 | }); 471 | }); 472 | 473 | // Create the service 474 | const service = micro((req, res) => router.handle(req, res)); 475 | 476 | // Listen to the service and make the request to route '/' 477 | const url = await listen(service); 478 | await request(url); 479 | 480 | router.unroute('/', 'GET'); 481 | 482 | // Perform the test check - route should no longer be registered 483 | try { 484 | await request(url); 485 | } catch (e) { 486 | if (e && e.statusCode && e.statusCode === 404) { 487 | t.pass(); 488 | } else { 489 | t.fail(); 490 | } 491 | } 492 | 493 | // Close the service 494 | service.close(); 495 | }); 496 | 497 | test('invalid route cannot be deregistered', async t => { 498 | // Create new instance of micro-http-router 499 | const router = new Router(); 500 | 501 | // Configure the routes 502 | router.get('/', (req, res) => { 503 | micro.send(res, 200, { 504 | success: true 505 | }); 506 | }); 507 | 508 | // Perform the test check - unroute should fail 509 | try { 510 | router.unroute('/foo', 'GET'); 511 | } catch (e) { 512 | t.pass(); 513 | } 514 | }); 515 | 516 | test('invalid route method cannot be deregistered', async t => { 517 | // Create new instance of micro-http-router 518 | const router = new Router(); 519 | 520 | // Configure the routes 521 | router.get('/', (req, res) => { 522 | micro.send(res, 200, { 523 | success: true 524 | }); 525 | }); 526 | 527 | // Perform the test check - unroute should fail 528 | try { 529 | router.unroute('/', 'POST'); 530 | } catch (e) { 531 | t.pass(); 532 | } 533 | }); 534 | 535 | test('unrouteAll method deregisters all routes successfully', async t => { 536 | // Create new instance of micro-http-router 537 | const router = new Router(); 538 | 539 | // Configure the routes 540 | router.get('/foo', (req, res) => { 541 | micro.send(res, 200, 'foo'); 542 | }); 543 | router.get('/bar', (req, res) => { 544 | micro.send(res, 200, 'bar'); 545 | }); 546 | router.get('/baz', (req, res) => { 547 | micro.send(res, 200, 'baz'); 548 | }); 549 | router.get('/qux', (req, res) => { 550 | micro.send(res, 200, 'qux'); 551 | }); 552 | 553 | // Perform test check - router should have 4 routes 554 | if (Object.keys(router.routes).length === 4) { 555 | 556 | router.unrouteAll(); 557 | // Router should now have zero routes 558 | if (Object.keys(router.routes).length === 0) { 559 | t.pass(); 560 | } else { 561 | t.fail(); 562 | } 563 | } else { 564 | t.fail(); 565 | } 566 | }); 567 | 568 | test('unroute does not delete path if methods still exist after unrouting', async t => { 569 | // Create new instance of micro-http-router 570 | const router = new Router(); 571 | 572 | // Configure the routes 573 | router.get('/foo', (req, res) => { 574 | micro.send(res, 200, 'foo'); 575 | }); 576 | router.post('/foo', (req, res) => { 577 | micro.send(res, 200, 'foo'); 578 | }); 579 | 580 | // Perform test check 581 | if (Object.keys(router.routes).length === 1) { 582 | 583 | router.unroute('/foo', 'POST'); 584 | // Router should still have one route 585 | if (Object.keys(router.routes).length === 1 && router.routes['/foo'].length === 1) { 586 | t.pass(); 587 | } else { 588 | t.fail(); 589 | } 590 | } else { 591 | t.fail(); 592 | } 593 | }); --------------------------------------------------------------------------------