├── .gitignore ├── .travis.yml ├── LICENSE ├── package.json ├── readme.md ├── rollup.config.ts ├── src └── router.ts ├── test ├── basic.ts └── functionality.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | # Dependency directory 17 | node_modules 18 | 19 | # Misc 20 | testing/ 21 | 22 | # Editors 23 | .idea 24 | *.iml 25 | .vscode 26 | 27 | # OS metadata 28 | .DS_Store 29 | Thumbs.db 30 | 31 | # Ignore built ts files 32 | dist/**/* 33 | 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - node 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 [fullname] 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. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-request-router", 3 | "version": "1.2.2", 4 | "description": "Fast, generic and type safe router (match method and path).", 5 | "author": "berstend", 6 | "license": "MIT", 7 | "repository": "berstend/tiny-request-router", 8 | "homepage": "https://github.com/berstend/tiny-request-router#readme", 9 | "main": "./dist/router.js", 10 | "module": "./dist/router.mjs", 11 | "unpkg": "./dist/router.min.js", 12 | "browser": { 13 | "./dist/router.js": "./dist/router.browser.js", 14 | "./dist/router.mjs": "./dist/router.browser.mjs" 15 | }, 16 | "types": "dist/src/router.d.ts", 17 | "files": [ 18 | "/dist" 19 | ], 20 | "scripts": { 21 | "check": "tsc --pretty --noEmit", 22 | "check:watch": "npm run check -- --watch", 23 | "declarations": "tsc --emitDeclarationOnly", 24 | "prebuild": "rimraf dist", 25 | "build": "rollup -c rollup.config.ts && npm run declarations", 26 | "dev": "npm run prebuild && rollup -c rollup.config.ts -w && npm run declarations", 27 | "test": "ava-ts -v", 28 | "prepublish": "npm run build", 29 | "docs": "documentation readme --quiet --shallow --github --markdown-theme transitivebs --readme-file readme.md --section API ./src/router.ts && npx prettier --write readme.md && npx prettier --write readme.md" 30 | }, 31 | "prettier": { 32 | "printWidth": 100, 33 | "semi": false, 34 | "singleQuote": true 35 | }, 36 | "keywords": [ 37 | "router", 38 | "service worker", 39 | "universal", 40 | "routing", 41 | "cloudflare", 42 | "worker", 43 | "browser", 44 | "node", 45 | "url router", 46 | "cloudflare worker", 47 | "typescript", 48 | "match request" 49 | ], 50 | "devDependencies": { 51 | "@types/node": "^12.12.14", 52 | "ava": "^2.4.0", 53 | "ava-ts": "^0.25.1", 54 | "documentation-markdown-themes": "^12.1.5", 55 | "rimraf": "^3.0.0", 56 | "rollup": "^1.27.5", 57 | "rollup-plugin-commonjs": "^10.1.0", 58 | "rollup-plugin-node-resolve": "^5.2.0", 59 | "rollup-plugin-terser": "^5.1.2", 60 | "rollup-plugin-typescript": "^1.0.1", 61 | "ts-node": "^8.5.4", 62 | "tslint": "^5.20.1", 63 | "tslint-config-prettier": "^1.15.0", 64 | "tslint-config-standard": "^9.0.0", 65 | "typescript": "^3.7.2" 66 | }, 67 | "dependencies": { 68 | "path-to-regexp": "^6.1.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # tiny-request-router [![ ](https://travis-ci.org/berstend/tiny-request-router.svg?branch=master)](https://travis-ci.org/berstend/tiny-request-router) [![ ](https://img.shields.io/npm/v/tiny-request-router.svg)](https://www.npmjs.com/package/tiny-request-router) 2 | 3 | > Fast, generic and type safe router (match request method and path). 4 | 5 | ## Features 6 | 7 | - Minimal and opinionless router, can be used in any script and environment. 8 | - Matches a request method (e.g. `GET`) and a path (e.g. `/foobar`) against a list of routes 9 | - Uses [path-to-regexp](https://github.com/pillarjs/path-to-regexp), which is used by express and therefore familiar 10 | - Allows wildcards (e.g. `/user/(.*)/age`) and named parameters (e.g. `/info/:username/:age`) 11 | - Will not call your handlers automatically, as it only cares about matching 12 | - Battle hardened in production ([Cloudflare Worker](https://www.cloudflare.com/products/cloudflare-workers/) with 10M requests per day) 13 | - No magic, no assumptions, no fluff, type safe, tested 14 | 15 | ### Route testing 16 | 17 | - You can use the [Express Route Tester](https://forbeslindesay.github.io/express-route-tester/) (select `2.0.0`) to debug your path patterns quickly 18 | 19 | ## Installation 20 | 21 | ```bash 22 | yarn add tiny-request-router 23 | # or 24 | npm install --save tiny-request-router 25 | ``` 26 | 27 | ## Usage (JavaScript/TypeScript) 28 | 29 | ```typescript 30 | import { Router } from 'tiny-request-router' 31 | // NodeJS: const { Router } = require('tiny-request-router') 32 | 33 | const router = new Router() 34 | 35 | router 36 | .get('/(v1|v2)/:name/:age', 'foo1') 37 | .get('/info/(.*)/export', 'foo2') 38 | .post('/upload/user', 'foo3') 39 | 40 | const match1 = router.match('GET', '/v1/') 41 | // => null 42 | 43 | const match2 = router.match('GET', '/v1/bob/22') 44 | // => { handler: 'foo1', params: { name: 'bob', age: '22' }, ... } 45 | ``` 46 | 47 | ### Make your handlers type safe (TypeScript) 48 | 49 | ```typescript 50 | import { Router, Method, Params } from 'tiny-request-router' 51 | 52 | // Let the router know that handlers are async functions returning a Response 53 | type Handler = (params: Params) => Promise 54 | 55 | const router = new Router() 56 | router.all('*', async () => new Response('Hello')) 57 | 58 | const match = router.match('GET' as Method, '/foobar') 59 | if (match) { 60 | // Call the async function of that match 61 | const response = await match.handler() 62 | console.log(response) // => Response('Hello') 63 | } 64 | ``` 65 | 66 | ## Example: Cloudflare Workers (JavaScript) 67 | 68 | _Use something like [wrangler](https://github.com/cloudflare/wrangler) to bundle the router with your worker code._ 69 | 70 | ```js 71 | import { Router } from 'tiny-request-router' 72 | 73 | const router = new Router() 74 | router.get('/worker', async () => new Response('Hi from worker!')) 75 | router.get('/hello/:name', async params => new Response(`Hello ${params.name}!`)) 76 | router.post('/test', async () => new Response('Post received!')) 77 | 78 | // Main entry point in workers 79 | addEventListener('fetch', event => { 80 | const request = event.request 81 | const { pathname } = new URL(request.url) 82 | 83 | const match = router.match(request.method, pathname) 84 | if (match) { 85 | event.respondWith(match.handler(match.params)) 86 | } 87 | }) 88 | ``` 89 | 90 | --- 91 | 92 | ## API 93 | 94 | 95 | 96 | #### Table of Contents 97 | 98 | - [Method()](#method) 99 | - [RouteOptions()](#routeoptions) 100 | - [RouteMatch()](#routematch) 101 | - [class: Router](#class-router) 102 | - [.routes](#routes) 103 | - [.all(path, handler, options)](#allpath-handler-options) 104 | - [.get(path, handler, options)](#getpath-handler-options) 105 | - [.post(path, handler, options)](#postpath-handler-options) 106 | - [.put(path, handler, options)](#putpath-handler-options) 107 | - [.patch(path, handler, options)](#patchpath-handler-options) 108 | - [.delete(path, handler, options)](#deletepath-handler-options) 109 | - [.head(path, handler, options)](#headpath-handler-options) 110 | - [.options(path, handler, options)](#optionspath-handler-options) 111 | - [.match(method, path)](#matchmethod-path) 112 | 113 | ### [Method()](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L6-L6) 114 | 115 | Type: **(`"GET"` \| `"POST"` \| `"PUT"` \| `"PATCH"` \| `"DELETE"` \| `"HEAD"` \| `"OPTIONS"`)** 116 | 117 | Valid HTTP methods for matching. 118 | 119 | --- 120 | 121 | ### [RouteOptions()](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L39-L39) 122 | 123 | **Extends: TokensToRegexpOptions** 124 | 125 | Optional route options. 126 | 127 | Example: 128 | 129 | ```javascript 130 | // When `true` the regexp will be case sensitive. (default: `false`) 131 | sensitive?: boolean; 132 | 133 | // When `true` the regexp allows an optional trailing delimiter to match. (default: `false`) 134 | strict?: boolean; 135 | 136 | // When `true` the regexp will match to the end of the string. (default: `true`) 137 | end?: boolean; 138 | 139 | // When `true` the regexp will match from the beginning of the string. (default: `true`) 140 | start?: boolean; 141 | 142 | // Sets the final character for non-ending optimistic matches. (default: `/`) 143 | delimiter?: string; 144 | 145 | // List of characters that can also be "end" characters. 146 | endsWith?: string; 147 | 148 | // Encode path tokens for use in the `RegExp`. 149 | encode?: (value: string) => string; 150 | ``` 151 | 152 | --- 153 | 154 | ### [RouteMatch()](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L67-L70) 155 | 156 | **Extends: Route<HandlerType>** 157 | 158 | The object returned when a route matches. 159 | 160 | The handler can then be used to execute the relevant function. 161 | 162 | Example: 163 | 164 | ```javascript 165 | { 166 | params: Params 167 | matches?: RegExpExecArray 168 | method: Method | MethodWildcard 169 | path: string 170 | regexp: RegExp 171 | options: RouteOptions 172 | keys: Keys 173 | handler: HandlerType 174 | } 175 | ``` 176 | 177 | --- 178 | 179 | ### class: [Router](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L86-L168) 180 | 181 | Tiny request router. Allows overloading of handler type to be fully type safe. 182 | 183 | Example: 184 | 185 | ```javascript 186 | import { Router, Method, Params } from 'tiny-request-router' 187 | 188 | // Let the router know that handlers are async functions returning a Response 189 | type Handler = (params: Params) => Promise 190 | 191 | const router = new Router() 192 | ``` 193 | 194 | --- 195 | 196 | #### .[routes](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L88-L88) 197 | 198 | List of all registered routes. 199 | 200 | --- 201 | 202 | #### .[all(path, handler, options)](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L91-L93) 203 | 204 | - `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 205 | - `handler` **HandlerType** 206 | - `options` **[RouteOptions](#routeoptions)** (optional, default `{}`) 207 | 208 | Add a route that matches any method. 209 | 210 | --- 211 | 212 | #### .[get(path, handler, options)](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L95-L97) 213 | 214 | - `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 215 | - `handler` **HandlerType** 216 | - `options` **[RouteOptions](#routeoptions)** (optional, default `{}`) 217 | 218 | Add a route that matches the GET method. 219 | 220 | --- 221 | 222 | #### .[post(path, handler, options)](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L99-L101) 223 | 224 | - `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 225 | - `handler` **HandlerType** 226 | - `options` **[RouteOptions](#routeoptions)** (optional, default `{}`) 227 | 228 | Add a route that matches the POST method. 229 | 230 | --- 231 | 232 | #### .[put(path, handler, options)](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L103-L105) 233 | 234 | - `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 235 | - `handler` **HandlerType** 236 | - `options` **[RouteOptions](#routeoptions)** (optional, default `{}`) 237 | 238 | Add a route that matches the PUT method. 239 | 240 | --- 241 | 242 | #### .[patch(path, handler, options)](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L107-L109) 243 | 244 | - `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 245 | - `handler` **HandlerType** 246 | - `options` **[RouteOptions](#routeoptions)** (optional, default `{}`) 247 | 248 | Add a route that matches the PATCH method. 249 | 250 | --- 251 | 252 | #### .[delete(path, handler, options)](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L111-L113) 253 | 254 | - `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 255 | - `handler` **HandlerType** 256 | - `options` **[RouteOptions](#routeoptions)** (optional, default `{}`) 257 | 258 | Add a route that matches the DELETE method. 259 | 260 | --- 261 | 262 | #### .[head(path, handler, options)](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L115-L117) 263 | 264 | - `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 265 | - `handler` **HandlerType** 266 | - `options` **[RouteOptions](#routeoptions)** (optional, default `{}`) 267 | 268 | Add a route that matches the HEAD method. 269 | 270 | --- 271 | 272 | #### .[options(path, handler, options)](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L119-L121) 273 | 274 | - `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 275 | - `handler` **HandlerType** 276 | - `options` **[RouteOptions](#routeoptions)** (optional, default `{}`) 277 | 278 | Add a route that matches the OPTIONS method. 279 | 280 | --- 281 | 282 | #### .[match(method, path)](https://github.com/berstend/tiny-request-router/blob/5e7d69be1e37a6d2d14c611efe77ba2ef6ea9f83/src/router.ts#L135-L152) 283 | 284 | - `method` **[Method](#method)** 285 | - `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 286 | 287 | Returns: **([RouteMatch](#routematch)<HandlerType> | null)** 288 | 289 | Match the provided method and path against the list of registered routes. 290 | 291 | Example: 292 | 293 | ```javascript 294 | router.get('/foobar', async () => new Response('Hello')) 295 | 296 | const match = router.match('GET', '/foobar') 297 | if (match) { 298 | // Call the async function of that match 299 | const response = await match.handler() 300 | console.log(response) // => Response('Hello') 301 | } 302 | ``` 303 | 304 | --- 305 | 306 | ## More info 307 | 308 | Please check out the [tiny source code](src/router.ts) or [tests](test/functionality.ts) for more info. 309 | 310 | ## License 311 | 312 | MIT 313 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs' 2 | import resolve from 'rollup-plugin-node-resolve' 3 | import { terser } from 'rollup-plugin-terser' 4 | import typescript from 'rollup-plugin-typescript' 5 | 6 | const pkg = require('./package.json') 7 | 8 | const pkgName = 'router' 9 | const umdName = 'TinyRequestRouter' 10 | const banner = ` 11 | /*! 12 | * ${pkg.name} v${pkg.version} by ${pkg.author} 13 | * ${pkg.homepage} 14 | * @license ${pkg.license} 15 | */ 16 | `.trim() 17 | 18 | export default [ 19 | /* router.js and router.mjs */ 20 | { 21 | input: `src/${pkgName}.ts`, 22 | output: [ 23 | { file: pkg.main, format: 'cjs', sourcemap: true, banner }, 24 | { file: pkg.module, format: 'esm', sourcemap: true, banner } 25 | ], 26 | plugins: [ 27 | resolve(), 28 | commonjs(), 29 | typescript({ 30 | typescript: require('typescript') 31 | }) 32 | ], 33 | external: ['url-pattern'] 34 | }, 35 | 36 | /* router.browser.js and router.browser.mjs */ 37 | { 38 | input: `src/${pkgName}.ts`, 39 | output: [ 40 | { 41 | file: pkg.browser[pkg.main], 42 | format: 'umd', 43 | name: umdName, 44 | sourcemap: true 45 | }, 46 | { 47 | file: pkg.browser[pkg.module], 48 | format: 'esm', 49 | sourcemap: true, 50 | banner 51 | } 52 | ], 53 | plugins: [ 54 | resolve({ browser: true }), 55 | commonjs(), 56 | typescript({ 57 | typescript: require('typescript') 58 | }) 59 | ] 60 | }, 61 | 62 | /* router.min.js */ 63 | { 64 | input: `src/${pkgName}.ts`, 65 | output: [ 66 | { 67 | file: pkg.unpkg, 68 | format: 'umd', 69 | name: umdName, 70 | sourcemap: true, 71 | banner 72 | } 73 | ], 74 | plugins: [ 75 | resolve({ browser: true }), 76 | commonjs(), 77 | typescript({ 78 | typescript: require('typescript') 79 | }), 80 | terser({ output: { comments: 'some' } }) 81 | ] 82 | } 83 | ] 84 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import { Key as TokenKey, pathToRegexp, TokensToRegexpOptions } from 'path-to-regexp' 2 | 3 | // https://basarat.gitbooks.io/typescript/docs/tips/barrel.html 4 | export { pathToRegexp } 5 | 6 | /** Valid HTTP methods for matching. */ 7 | export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' 8 | export type MethodWildcard = 'ALL' 9 | 10 | export interface Params { 11 | [key: string]: string 12 | } 13 | 14 | /** 15 | * Optional route options. 16 | * 17 | * @example 18 | * // When `true` the regexp will be case sensitive. (default: `false`) 19 | * sensitive?: boolean; 20 | * 21 | * // When `true` the regexp allows an optional trailing delimiter to match. (default: `false`) 22 | * strict?: boolean; 23 | * 24 | * // When `true` the regexp will match to the end of the string. (default: `true`) 25 | * end?: boolean; 26 | * 27 | * // When `true` the regexp will match from the beginning of the string. (default: `true`) 28 | * start?: boolean; 29 | * 30 | * // Sets the final character for non-ending optimistic matches. (default: `/`) 31 | * delimiter?: string; 32 | * 33 | * // List of characters that can also be "end" characters. 34 | * endsWith?: string; 35 | * 36 | * // Encode path tokens for use in the `RegExp`. 37 | * encode?: (value: string) => string; 38 | */ 39 | export interface RouteOptions extends TokensToRegexpOptions {} 40 | 41 | export interface Route { 42 | method: Method | MethodWildcard 43 | path: string 44 | regexp: RegExp 45 | options: RouteOptions 46 | keys: Keys 47 | handler: HandlerType 48 | } 49 | 50 | /** 51 | * The object returned when a route matches. 52 | * 53 | * The handler can then be used to execute the relevant function. 54 | * 55 | * @example 56 | * { 57 | * params: Params 58 | * matches?: RegExpExecArray 59 | * method: Method | MethodWildcard 60 | * path: string 61 | * regexp: RegExp 62 | * options: RouteOptions 63 | * keys: Keys 64 | * handler: HandlerType 65 | * } 66 | */ 67 | export interface RouteMatch extends Route { 68 | params: Params 69 | matches?: RegExpExecArray 70 | } 71 | 72 | export type Key = TokenKey 73 | export type Keys = Array 74 | 75 | /** 76 | * Tiny request router. Allows overloading of handler type to be fully type safe. 77 | * 78 | * @example 79 | * import { Router, Method, Params } from 'tiny-request-router' 80 | * 81 | * // Let the router know that handlers are async functions returning a Response 82 | * type Handler = (params: Params) => Promise 83 | * 84 | * const router = new Router() 85 | */ 86 | export class Router { 87 | /** List of all registered routes. */ 88 | public routes: Array> = [] 89 | 90 | /** Add a route that matches any method. */ 91 | public all(path: string, handler: HandlerType, options: RouteOptions = {}) { 92 | return this._push('ALL', path, handler, options) 93 | } 94 | /** Add a route that matches the GET method. */ 95 | public get(path: string, handler: HandlerType, options: RouteOptions = {}) { 96 | return this._push('GET', path, handler, options) 97 | } 98 | /** Add a route that matches the POST method. */ 99 | public post(path: string, handler: HandlerType, options: RouteOptions = {}) { 100 | return this._push('POST', path, handler, options) 101 | } 102 | /** Add a route that matches the PUT method. */ 103 | public put(path: string, handler: HandlerType, options: RouteOptions = {}) { 104 | return this._push('PUT', path, handler, options) 105 | } 106 | /** Add a route that matches the PATCH method. */ 107 | public patch(path: string, handler: HandlerType, options: RouteOptions = {}) { 108 | return this._push('PATCH', path, handler, options) 109 | } 110 | /** Add a route that matches the DELETE method. */ 111 | public delete(path: string, handler: HandlerType, options: RouteOptions = {}) { 112 | return this._push('DELETE', path, handler, options) 113 | } 114 | /** Add a route that matches the HEAD method. */ 115 | public head(path: string, handler: HandlerType, options: RouteOptions = {}) { 116 | return this._push('HEAD', path, handler, options) 117 | } 118 | /** Add a route that matches the OPTIONS method. */ 119 | public options(path: string, handler: HandlerType, options: RouteOptions = {}) { 120 | return this._push('OPTIONS', path, handler, options) 121 | } 122 | /** 123 | * Match the provided method and path against the list of registered routes. 124 | * 125 | * @example 126 | * router.get('/foobar', async () => new Response('Hello')) 127 | * 128 | * const match = router.match('GET', '/foobar') 129 | * if (match) { 130 | * // Call the async function of that match 131 | * const response = await match.handler() 132 | * console.log(response) // => Response('Hello') 133 | * } 134 | */ 135 | public match(method: Method, path: string): RouteMatch | null { 136 | for (const route of this.routes) { 137 | // Skip immediately if method doesn't match 138 | if (route.method !== method && route.method !== 'ALL') continue 139 | // Speed optimizations for catch all wildcard routes 140 | if (route.path === '(.*)') { 141 | return { ...route, params: { '0': route.path } } 142 | } 143 | if (route.path === '/' && route.options.end === false) { 144 | return { ...route, params: {} } 145 | } 146 | // If method matches try to match path regexp 147 | const matches = route.regexp.exec(path) 148 | if (!matches || !matches.length) continue 149 | return { ...route, matches, params: keysToParams(matches, route.keys) } 150 | } 151 | return null 152 | } 153 | 154 | private _push( 155 | method: Method | MethodWildcard, 156 | path: string, 157 | handler: HandlerType, 158 | options: RouteOptions 159 | ) { 160 | const keys: Keys = [] 161 | if (path === '*') { 162 | path = '(.*)' 163 | } 164 | const regexp = pathToRegexp(path, keys, options) 165 | this.routes.push({ method, path, handler, keys, options, regexp }) 166 | return this 167 | } 168 | } 169 | 170 | // Convert an array of keys and matches to a params object 171 | const keysToParams = (matches: RegExpExecArray, keys: Keys): Params => { 172 | const params: Params = {} 173 | for (let i = 1; i < matches.length; i++) { 174 | const key = keys[i - 1] 175 | const prop = key.name 176 | const val = matches[i] 177 | if (val !== undefined) { 178 | params[prop] = val 179 | } 180 | } 181 | return params 182 | } 183 | -------------------------------------------------------------------------------- /test/basic.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { Router } from '../src/router' 4 | 5 | test('is class', t => { 6 | t.is(typeof Router, 'function') 7 | }) 8 | 9 | test('should have the basic class members', async t => { 10 | const instance = new Router() 11 | 12 | t.true(instance.all instanceof Function) 13 | t.true(instance.get instanceof Function) 14 | t.true(instance.post instanceof Function) 15 | t.true(instance.put instanceof Function) 16 | t.true(instance.patch instanceof Function) 17 | t.true(instance.delete instanceof Function) 18 | t.true(instance.head instanceof Function) 19 | t.true(instance.options instanceof Function) 20 | 21 | t.true(instance.match instanceof Function) 22 | }) 23 | -------------------------------------------------------------------------------- /test/functionality.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { Router } from '../src/router' 4 | 5 | test('should match route with wildcard', async t => { 6 | const router = new Router() 7 | 8 | router 9 | .head('*', 'foo1') 10 | .get('/', 'foo2') 11 | .get('/foo', 'foo3') 12 | 13 | const match1 = router.match('HEAD', '/foo') 14 | const match2 = router.match('GET', '/') 15 | const match3 = router.match('GET', '/foo') 16 | const match4 = router.match('POST', '/foo') 17 | 18 | t.true(match1 instanceof Object) 19 | t.true(match2 instanceof Object) 20 | t.true(match3 instanceof Object) 21 | t.false(match4 instanceof Object) 22 | 23 | if (match1) { 24 | t.is(match1.handler, 'foo1') 25 | } 26 | if (match2) { 27 | t.is(match2.handler, 'foo2') 28 | } 29 | if (match3) { 30 | t.is(match3.handler, 'foo3') 31 | 32 | t.true(match3.matches && match3.matches.length === 1) 33 | t.is(match3.method, 'GET') 34 | t.deepEqual(match3.options, {}) 35 | t.deepEqual(match3.params, {}) 36 | t.is(match3.path, '/foo') 37 | } 38 | }) 39 | 40 | test('should match wildcard OPTIONS', async t => { 41 | const router = new Router() 42 | 43 | router.options('*', 'foo1') 44 | 45 | const match1 = router.match('GET', '/foo') 46 | const match2 = router.match('OPTIONS', '/foo') 47 | 48 | t.false(match1 instanceof Object) 49 | t.true(match2 instanceof Object) 50 | 51 | if (match2) { 52 | t.is(match2.handler, 'foo1') 53 | } 54 | }) 55 | 56 | test('should match wildcard method', async t => { 57 | const router = new Router() 58 | 59 | router.all('/secret', 'foo1') 60 | 61 | const match1 = router.match('GET', '/foo') 62 | const match2 = router.match('OPTIONS', '/secret') 63 | 64 | t.false(match1 instanceof Object) 65 | t.true(match2 instanceof Object) 66 | 67 | if (match2) { 68 | t.is(match2.handler, 'foo1') 69 | } 70 | }) 71 | 72 | test('should match route with named params', async t => { 73 | const router = new Router() 74 | 75 | router 76 | .get('/(v1|v2)/:name/:age', 'foo1') // ok! 77 | // .get('/(v1|v2)/:name/:age/*', 'foo2') // Not ok! 78 | .get('/(v1|v2)/:name/:age/(.*)', 'foo3') // ok! 79 | .get('/v2/:name/:age', 'foo4') 80 | .get('/v3/alice', 'foo5') 81 | 82 | const match1 = router.match('GET', '/v1/') 83 | const match2 = router.match('GET', '/v1/bob/22') 84 | const match3 = router.match('GET', '/v1/bob/22/hello') 85 | const match4 = router.match('GET', '/v2/keith,89') 86 | 87 | t.false(match1 instanceof Object) 88 | t.true(match2 instanceof Object) 89 | t.true(match3 instanceof Object) 90 | t.false(match4 instanceof Object) 91 | 92 | if (match2) { 93 | t.is(match2.handler, 'foo1') 94 | t.is(match2.params.name, 'bob') 95 | t.is(match2.params.age, '22') 96 | t.is(match2.params['0'], 'v1') 97 | } 98 | if (match3) { 99 | t.is(match3.handler, 'foo3') 100 | t.is(match3.params.name, 'bob') 101 | t.is(match3.params.age, '22') 102 | t.is(match3.params['0'], 'v1') 103 | t.is(match3.params['1'], 'hello') 104 | } 105 | }) 106 | 107 | test('should not be case sensitive by default', async t => { 108 | // By default not case sensitive 109 | const router = new Router() 110 | router.all('/:name/LOCATION', 'foo1') 111 | const match1 = router.match('GET', '/bob/') 112 | const match2 = router.match('GET', '/bob/location') 113 | const match3 = router.match('GET', '/bob/LOCATION') 114 | t.false(match1 instanceof Object) 115 | t.true(match2 instanceof Object) 116 | t.true(match3 instanceof Object) 117 | }) 118 | 119 | test('should allow options to be case sensitive', async t => { 120 | // By default not case sensitive 121 | const router = new Router() 122 | router.all('/:name/LOCATION', 'foo1', { sensitive: true }) 123 | const match1 = router.match('GET', '/bob/') 124 | const match2 = router.match('GET', '/bob/location') 125 | const match3 = router.match('GET', '/bob/LOCATION') 126 | t.false(match1 instanceof Object) 127 | t.false(match2 instanceof Object) 128 | t.true(match3 instanceof Object) 129 | }) 130 | 131 | type HandlerType = () => Response 132 | 133 | test('should allow functions as handlers', async t => { 134 | const router = new Router() 135 | router.all('*', () => new Response('Hello')) 136 | 137 | const match1 = router.match('GET', '/foo') 138 | t.true(match1 instanceof Object) 139 | }) 140 | 141 | test('should allow async functions as handlers', async t => { 142 | const router = new Router<() => Promise>() 143 | router.all('*', async () => 123) 144 | 145 | const match = router.match('GET', '/foo') 146 | t.true(match instanceof Object) 147 | 148 | if (match) { 149 | const response = await match.handler() 150 | console.log(response) 151 | // => Response('Hello') 152 | } 153 | }) 154 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "target": "es3", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "allowSyntheticDefaultImports": true, 9 | "esModuleInterop": true, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "strict": true, 13 | "lib": ["es2015", "webworker"], 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitReturns": false, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": false, 19 | "pretty": true, 20 | "stripInternal": true 21 | }, 22 | "include": ["./src/**/*.tsx", "./src/**/*.ts", "./test/**/*.ts"], 23 | "exclude": ["node_modules", "dist", "./test/**/*.spec.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-standard", "tslint-config-prettier"], 3 | "rules": { 4 | "ordered-imports": true 5 | } 6 | } 7 | --------------------------------------------------------------------------------