You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
562 lines
22 KiB
562 lines
22 KiB
# find-my-way
|
|
|
|
[](http://standardjs.com/)  [](https://www.npmjs.com/package/find-my-way)
|
|
|
|
A crazy fast HTTP router, internally uses an highly performant [Radix Tree](https://en.wikipedia.org/wiki/Radix_tree) (aka compact [Prefix Tree](https://en.wikipedia.org/wiki/Trie)), supports route params, wildcards, and it's framework independent.
|
|
|
|
If you want to see a benchmark comparison with the most commonly used routers, see [here](https://github.com/delvedor/router-benchmark).<br>
|
|
Do you need a real-world example that uses this router? Check out [Fastify](https://github.com/fastify/fastify) or [Restify](https://github.com/restify/node-restify).
|
|
|
|
<a name="install"></a>
|
|
## Install
|
|
```
|
|
npm i find-my-way --save
|
|
```
|
|
|
|
<a name="usage"></a>
|
|
## Usage
|
|
```js
|
|
const http = require('http')
|
|
const router = require('find-my-way')()
|
|
|
|
router.on('GET', '/', (req, res, params) => {
|
|
res.end('{"message":"hello world"}')
|
|
})
|
|
|
|
const server = http.createServer((req, res) => {
|
|
router.lookup(req, res)
|
|
})
|
|
|
|
server.listen(3000, err => {
|
|
if (err) throw err
|
|
console.log('Server listening on: http://localhost:3000')
|
|
})
|
|
```
|
|
|
|
<a name="api"></a>
|
|
## API
|
|
<a name="constructor"></a>
|
|
#### FindMyWay([options])
|
|
Instance a new router.<br>
|
|
You can pass a default route with the option `defaultRoute`.
|
|
```js
|
|
const router = require('find-my-way')({
|
|
defaultRoute: (req, res) => {
|
|
res.statusCode = 404
|
|
res.end()
|
|
}
|
|
})
|
|
```
|
|
|
|
In case of a badly formatted url *(eg: `/hello/%world`)*, by default `find-my-way` will invoke the `defaultRoute`, unless you specify the `onBadUrl` option:
|
|
```js
|
|
const router = require('find-my-way')({
|
|
onBadUrl: (path, req, res) => {
|
|
res.statusCode = 400
|
|
res.end(`Bad path: ${path}`)
|
|
}
|
|
})
|
|
```
|
|
|
|
Trailing slashes can be ignored by supplying the `ignoreTrailingSlash` option:
|
|
```js
|
|
const router = require('find-my-way')({
|
|
ignoreTrailingSlash: true
|
|
})
|
|
function handler (req, res, params) {
|
|
res.end('foo')
|
|
}
|
|
// maps "/foo/" and "/foo" to `handler`
|
|
router.on('GET', '/foo/', handler)
|
|
```
|
|
|
|
You can set a custom length for parameters in parametric *(standard, regex and multi)* routes by using `maxParamLength` option, the default value is 100 characters.<br/>
|
|
*If the maximum length limit is reached, the default route will be invoked.*
|
|
```js
|
|
const router = require('find-my-way')({
|
|
maxParamLength: 500
|
|
})
|
|
```
|
|
|
|
If you are using a regex based route, `find-my-way` will throw an error if detects potentially catastrophic exponential-time regular expressions *(internally uses [`safe-regex2`](https://github.com/fastify/safe-regex2))*.<br/>
|
|
If you want to disable this behavior, pass the option `allowUnsafeRegex`.
|
|
```js
|
|
const router = require('find-my-way')({
|
|
allowUnsafeRegex: true
|
|
})
|
|
```
|
|
|
|
According to [RFC3986](https://tools.ietf.org/html/rfc3986#section-6.2.2.1), find-my-way is case sensitive by default.
|
|
You can disable this by setting the `caseSensitive` option to `false`:
|
|
in that case, all paths will be matched as lowercase, but the route parameters or wildcards will maintain their original letter casing. You can turn off case sensitivity with:
|
|
|
|
```js
|
|
const router = require('find-my-way')({
|
|
caseSensitive: false
|
|
})
|
|
```
|
|
|
|
You can assign a `buildPrettyMeta` function to sanitize a route's `store` object to use with the `prettyPrint` functions. This function should accept a single object and return an object.
|
|
|
|
```js
|
|
|
|
const privateKey = new Symbol('private key')
|
|
const store = { token: '12345', [privateKey]: 'private value' }
|
|
|
|
const router = require('find-my-way')({
|
|
buildPrettyMeta: route => {
|
|
const cleanMeta = Object.assign({}, route.store)
|
|
|
|
// remove private properties
|
|
Object.keys(cleanMeta).forEach(k => {
|
|
if (typeof k === 'symbol') delete cleanMeta[k]
|
|
})
|
|
|
|
return cleanMeta // this will show up in the pretty print output!
|
|
}
|
|
})
|
|
|
|
store[privateKey] = 'private value'
|
|
router.on('GET', '/hello_world', (req, res) => {}, store)
|
|
|
|
router.prettyPrint()
|
|
|
|
//└── / (-)
|
|
// └── hello_world (GET)
|
|
// • (token) "12345"
|
|
|
|
```
|
|
|
|
## Constraints
|
|
|
|
`find-my-way` supports restricting handlers to only match certain requests for the same path. This can be used to support different versions of the same route that conform to a [semver](#semver) based versioning strategy, or restricting some routes to only be available on hosts. `find-my-way` has the semver based versioning strategy and a regex based hostname constraint strategy built in.
|
|
|
|
To constrain a route to only match sometimes, pass `constraints` to the route options when registering the route:
|
|
|
|
```js
|
|
findMyWay.on('GET', '/', { constraints: { version: '1.0.2' } }, (req, res) => {
|
|
// will only run when the request's Accept-Version header asks for a version semver compatible with 1.0.2, like 1.x, or 1.0.x.
|
|
})
|
|
|
|
findMyWay.on('GET', '/', { constraints: { host: 'example.com' } }, (req, res) => {
|
|
// will only run when the request's Host header is `example.com`
|
|
})
|
|
```
|
|
|
|
Constraints can be combined, and route handlers will only match if __all__ of the constraints for the handler match the request. `find-my-way` does a boolean AND with each route constraint, not an OR.
|
|
|
|
`find-my-way` will try to match the most constrained handlers first before handler with fewer or no constraints.
|
|
|
|
<a name="custom-constraint-strategies"></a>
|
|
### Custom Constraint Strategies
|
|
|
|
Custom constraining strategies can be added and are matched against incoming requests while trying to maintain `find-my-way`'s high performance. To register a new type of constraint, you must add a new constraint strategy that knows how to match values to handlers, and that knows how to get the constraint value from a request. Register strategies when constructing a router:
|
|
|
|
```js
|
|
const customResponseTypeStrategy = {
|
|
// strategy name for referencing in the route handler `constraints` options
|
|
name: 'accept',
|
|
// storage factory for storing routes in the find-my-way route tree
|
|
storage: function () {
|
|
let handlers = {}
|
|
return {
|
|
get: (type) => { return handlers[type] || null },
|
|
set: (type, store) => { handlers[type] = store },
|
|
del: (type) => { delete handlers[type] },
|
|
empty: () => { handlers = {} }
|
|
}
|
|
},
|
|
// function to get the value of the constraint from each incoming request
|
|
deriveConstraint: (req, ctx) => {
|
|
return req.headers['accept']
|
|
},
|
|
// optional flag marking if handlers without constraints can match requests that have a value for this constraint
|
|
mustMatchWhenDerived: true
|
|
}
|
|
|
|
const router = FindMyWay({ constraints: { accept: customResponseTypeStrategy } });
|
|
```
|
|
|
|
Once a custom constraint strategy is registered, routes can be added that are constrained using it:
|
|
|
|
|
|
```js
|
|
findMyWay.on('GET', '/', { constraints: { accept: 'application/fancy+json' } }, (req, res) => {
|
|
// will only run when the request's Accept header asks for 'application/fancy+json'
|
|
})
|
|
|
|
findMyWay.on('GET', '/', { constraints: { accept: 'application/fancy+xml' } }, (req, res) => {
|
|
// will only run when the request's Accept header asks for 'application/fancy+xml'
|
|
})
|
|
```
|
|
|
|
Constraint strategies should be careful to make the `deriveConstraint` function performant as it is run for every request matched by the router. See the `lib/strategies` directory for examples of the built in constraint strategies.
|
|
|
|
|
|
<a name="custom-versioning"></a>
|
|
By default, `find-my-way` uses a built in strategies for the version constraint that uses semantic version based matching logic, which is detailed [below](#semver). It is possible to define an alternative strategy:
|
|
|
|
```js
|
|
const customVersioning = {
|
|
// replace the built in version strategy
|
|
name: 'version',
|
|
// provide a storage factory to store handlers in a simple way
|
|
storage: function () {
|
|
let versions = {}
|
|
return {
|
|
get: (version) => { return versions[version] || null },
|
|
set: (version, store) => { versions[version] = store },
|
|
del: (version) => { delete versions[version] },
|
|
empty: () => { versions = {} }
|
|
}
|
|
},
|
|
deriveConstraint: (req, ctx) => {
|
|
return req.headers['accept']
|
|
},
|
|
mustMatchWhenDerived: true // if the request is asking for a version, don't match un-version-constrained handlers
|
|
}
|
|
|
|
const router = FindMyWay({ constraints: { version: customVersioning } });
|
|
```
|
|
|
|
The custom strategy object should contain next properties:
|
|
* `storage` - a factory function to store lists of handlers for each possible constraint value. The storage object can use domain-specific storage mechanisms to store handlers in a way that makes sense for the constraint at hand. See `lib/strategies` for examples, like the `version` constraint strategy that matches using semantic versions, or the `host` strategy that allows both exact and regex host constraints.
|
|
* `deriveConstraint` - the function to determine the value of this constraint given a request
|
|
|
|
The signature of the functions and objects must match the one from the example above.
|
|
|
|
*Please, be aware, if you use your own constraining strategy - you use it on your own risk. This can lead both to the performance degradation and bugs which are not related to `find-my-way` itself!*
|
|
|
|
<a name="on"></a>
|
|
#### on(method, path, [opts], handler, [store])
|
|
Register a new route.
|
|
```js
|
|
router.on('GET', '/example', (req, res, params) => {
|
|
// your code
|
|
})
|
|
```
|
|
Last argument, `store` is used to pass an object that you can access later inside the handler function. If needed, `store` can be updated.
|
|
```js
|
|
router.on('GET', '/example', (req, res, params, store) => {
|
|
assert.equal(store, { message: 'hello world' })
|
|
}, { message: 'hello world' })
|
|
```
|
|
|
|
##### Versioned routes
|
|
|
|
If needed, you can provide a `version` route constraint, which will allow you to declare multiple versions of the same route that are used selectively when requests ask for different version using the `Accept-Version` header. This is useful if you want to support several different behaviours for a given route and different clients select among them.
|
|
|
|
If you never configure a versioned route, the `'Accept-Version'` header will be ignored. Remember to set a [Vary](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) header in your responses with the value you are using for deifning the versioning (e.g.: 'Accept-Version'), to prevent cache poisoning attacks. You can also configure this as part your Proxy/CDN.
|
|
|
|
###### default
|
|
<a name="semver"></a>
|
|
The default versioning strategy follows the [semver](https://semver.org/) specification. When using `lookup`, `find-my-way` will automatically detect the `Accept-Version` header and route the request accordingly. Internally `find-my-way` uses the [`semver-store`](https://github.com/delvedor/semver-store) to get the correct version of the route; *advanced ranges* and *pre-releases* currently are not supported.
|
|
|
|
*Be aware that using this feature will cause a degradation of the overall performances of the router.*
|
|
|
|
```js
|
|
router.on('GET', '/example', { constraints: { version: '1.2.0' }}, (req, res, params) => {
|
|
res.end('Hello from 1.2.0!')
|
|
})
|
|
|
|
router.on('GET', '/example', { constraints: { version: '2.4.0' }}, (req, res, params) => {
|
|
res.end('Hello from 2.4.0!')
|
|
})
|
|
|
|
// The 'Accept-Version' header could be '1.2.0' as well as '*', '2.x' or '2.4.x'
|
|
```
|
|
|
|
If you declare multiple versions with the same *major* or *minor* `find-my-way` will always choose the highest compatible with the `Accept-Version` header value.
|
|
|
|
###### custom
|
|
It's also possible to define a [custom versioning strategy](#custom-versioning) during the `find-my-way` initialization. In this case the logic of matching the request to the specific handler depends on the versioning strategy you use.
|
|
|
|
##### on(methods[], path, [opts], handler, [store])
|
|
Register a new route for each method specified in the `methods` array.
|
|
It comes handy when you need to declare multiple routes with the same handler but different methods.
|
|
```js
|
|
router.on(['GET', 'POST'], '/example', (req, res, params) => {
|
|
// your code
|
|
})
|
|
```
|
|
|
|
<a name="supported-path-formats"></a>
|
|
##### Supported path formats
|
|
To register a **parametric** path, use the *colon* before the parameter name. For **wildcard** use the *star*.
|
|
*Remember that static routes are always inserted before parametric and wildcard.*
|
|
|
|
```js
|
|
// parametric
|
|
router.on('GET', '/example/:userId', (req, res, params) => {}))
|
|
router.on('GET', '/example/:userId/:secretToken', (req, res, params) => {}))
|
|
|
|
// wildcard
|
|
router.on('GET', '/example/*', (req, res, params) => {}))
|
|
```
|
|
|
|
Regular expression routes are supported as well, but pay attention, RegExp are very expensive in term of performance!<br>
|
|
If you want to declare a regular expression route, you must put the regular expression inside round parenthesis after the parameter name.
|
|
```js
|
|
// parametric with regexp
|
|
router.on('GET', '/example/:file(^\\d+).png', () => {}))
|
|
```
|
|
|
|
It's possible to define more than one parameter within the same couple of slash ("/"). Such as:
|
|
```js
|
|
router.on('GET', '/example/near/:lat-:lng/radius/:r', (req, res, params) => {}))
|
|
```
|
|
*Remember in this case to use the dash ("-") as parameters separator.*
|
|
|
|
Finally it's possible to have multiple parameters with RegExp.
|
|
```js
|
|
router.on('GET', '/example/at/:hour(^\\d{2})h:minute(^\\d{2})m', (req, res, params) => {}))
|
|
```
|
|
In this case as parameter separator it's possible to use whatever character is not matched by the regular expression.
|
|
|
|
The last parameter can be made optional if you add a question mark ("?") at the end of the parameters name.
|
|
```js
|
|
router.on('GET', '/example/posts/:id?', (req, res, params) => {}))
|
|
```
|
|
In this case you can request `/example/posts` as well as `/example/posts/1`. The optional param will be undefined if not specified.
|
|
|
|
Having a route with multiple parameters may affect negatively the performance, so prefer single parameter approach whenever possible, especially on routes which are on the hot path of your application.
|
|
|
|
<a name="match-order"></a>
|
|
##### Match order
|
|
|
|
The routing algorithm matches one chunk at a time (where the chunk is a string between two slashes),
|
|
this means that it cannot know if a route is static or dynamic until it finishes to match the URL.
|
|
|
|
The chunks are matched in the following order:
|
|
|
|
1. static
|
|
1. parametric
|
|
1. wildcards
|
|
1. parametric(regex)
|
|
1. multi parametric(regex)
|
|
|
|
So if you declare the following routes
|
|
|
|
- `/:userId/foo/bar`
|
|
- `/33/:a(^.*$)/:b`
|
|
|
|
and the URL of the incoming request is /33/foo/bar,
|
|
the second route will be matched because the first chunk (33) matches the static chunk.
|
|
If the URL would have been /32/foo/bar, the first route would have been matched.
|
|
Once a url has been matched, `find-my-way` will figure out which handler registered for that path matches the request if there are any constraints.
|
|
`find-my-way` will check the most constrained handlers first, which means the handlers with the most keys in the `constraints` object.
|
|
|
|
> If you just want a path containing a colon without declaring a parameter, use a double colon.
|
|
> For example, `/name::customVerb` will be interpreted as `/name:customVerb`
|
|
|
|
<a name="supported-methods"></a>
|
|
##### Supported methods
|
|
The router is able to route all HTTP methods defined by [`http` core module](https://nodejs.org/api/http.html#http_http_methods).
|
|
|
|
<a name="off"></a>
|
|
#### off(method, path)
|
|
Deregister a route.
|
|
```js
|
|
router.off('GET', '/example')
|
|
// => { handler: Function, params: Object, store: Object}
|
|
// => null
|
|
```
|
|
|
|
##### off(methods[], path, handler, [store])
|
|
Deregister a route for each method specified in the `methods` array.
|
|
It comes handy when you need to deregister multiple routes with the same path but different methods.
|
|
```js
|
|
router.off(['GET', 'POST'], '/example')
|
|
// => [{ handler: Function, params: Object, store: Object}]
|
|
// => null
|
|
```
|
|
|
|
<a name="reset"></a>
|
|
#### reset()
|
|
Empty router.
|
|
```js
|
|
router.reset()
|
|
```
|
|
|
|
##### Caveats
|
|
* It's not possible to register two routes which differs only for their parameters, because internally they would be seen as the same route. In a such case you'll get an early error during the route registration phase. An example is worth thousand words:
|
|
```js
|
|
const findMyWay = FindMyWay({
|
|
defaultRoute: (req, res) => {}
|
|
})
|
|
|
|
findMyWay.on('GET', '/user/:userId(^\\d+)', (req, res, params) => {})
|
|
|
|
findMyWay.on('GET', '/user/:username(^[a-z]+)', (req, res, params) => {})
|
|
// Method 'GET' already declared for route ':'
|
|
```
|
|
|
|
<a name="shorthand-methods"></a>
|
|
##### Shorthand methods
|
|
If you want an even nicer api, you can also use the shorthand methods to declare your routes.
|
|
|
|
For each HTTP supported method, there's the shorthand method. For example:
|
|
```js
|
|
router.get(path, handler [, store])
|
|
router.delete(path, handler [, store])
|
|
router.head(path, handler [, store])
|
|
router.patch(path, handler [, store])
|
|
router.post(path, handler [, store])
|
|
router.put(path, handler [, store])
|
|
router.options(path, handler [, store])
|
|
// ...
|
|
```
|
|
|
|
If you need a route that supports *all* methods you can use the `all` api.
|
|
```js
|
|
router.all(path, handler [, store])
|
|
```
|
|
|
|
<a name="lookup"></a>
|
|
#### lookup(request, response, [context])
|
|
Start a new search, `request` and `response` are the server req/res objects.<br>
|
|
If a route is found it will automatically call the handler, otherwise the default route will be called.<br>
|
|
The url is sanitized internally, all the parameters and wildcards are decoded automatically.
|
|
```js
|
|
router.lookup(req, res)
|
|
```
|
|
|
|
`lookup` accepts an optional context which will be the value of `this` when executing a handler
|
|
```js
|
|
router.on('GET', '*', function(req, res) {
|
|
res.end(this.greeting);
|
|
})
|
|
router.lookup(req, res, { greeting: 'Hello, World!' })
|
|
```
|
|
|
|
<a name="find"></a>
|
|
#### find(method, path, [constraints])
|
|
Return (if present) the route registered in *method:path*.<br>
|
|
The path must be sanitized, all the parameters and wildcards are decoded automatically.<br/>
|
|
An object with routing constraints should usually be passed as `constraints`, containing keys like the `host` for the request, the `version` for the route to be matched, or other custom constraint values. If the router is using the default versioning strategy, the version value should be conform to the [semver](https://semver.org/) specification. If you want to use the existing constraint strategies to derive the constraint values from an incoming request, use `lookup` instead of `find`. If no value is passed for `constraints`, the router won't match any constrained routes. If using constrained routes, passing `undefined` for the constraints leads to undefined behavior and should be avoided.
|
|
|
|
```js
|
|
router.find('GET', '/example', { host: 'fastify.io' })
|
|
// => { handler: Function, params: Object, store: Object}
|
|
// => null
|
|
|
|
router.find('GET', '/example', { host: 'fastify.io', version: '1.x' })
|
|
// => { handler: Function, params: Object, store: Object}
|
|
// => null
|
|
```
|
|
|
|
<a name="pretty-print"></a>
|
|
#### prettyPrint([{ commonPrefix: false, includeMeta: true || [] }])
|
|
Prints the representation of the internal radix tree, useful for debugging.
|
|
|
|
```js
|
|
findMyWay.on('GET', '/test', () => {})
|
|
findMyWay.on('GET', '/test/hello', () => {})
|
|
findMyWay.on('GET', '/testing', () => {})
|
|
findMyWay.on('GET', '/testing/:param', () => {})
|
|
findMyWay.on('PUT', '/update', () => {})
|
|
|
|
console.log(findMyWay.prettyPrint())
|
|
// └── /
|
|
// ├── test (GET)
|
|
// │ ├── /hello (GET)
|
|
// │ └── ing (GET)
|
|
// │ └── /:param (GET)
|
|
// └── update (PUT)
|
|
```
|
|
|
|
`prettyPrint` accepts an optional setting to use the internal routes array
|
|
to render the tree.
|
|
|
|
```js
|
|
console.log(findMyWay.prettyPrint({ commonPrefix: false }))
|
|
// └── / (-)
|
|
// ├── test (GET)
|
|
// │ └── /hello (GET)
|
|
// ├── testing (GET)
|
|
// │ └── /:param (GET)
|
|
// └── update (PUT)
|
|
```
|
|
|
|
To include a display of the `store` data passed to individual routes, the
|
|
option `includeMeta` may be passed. If set to `true` all items will be
|
|
displayed, this can also be set to an array specifying which keys (if
|
|
present) should be displayed. This information can be further sanitized
|
|
by specifying a `buildPrettyMeta` function which consumes and returns
|
|
an object.
|
|
|
|
```js
|
|
findMyWay.on('GET', '/test', () => {}, { onRequest: () => {}, authIDs => [1,2,3] })
|
|
findMyWay.on('GET', '/test/hello', () => {}, { token: 'df123-4567' })
|
|
findMyWay.on('GET', '/testing', () => {})
|
|
findMyWay.on('GET', '/testing/:param', () => {})
|
|
findMyWay.on('PUT', '/update', () => {})
|
|
|
|
console.log(findMyWay.prettyPrint({ commonPrefix: false, includeMeta: ['onRequest'] }))
|
|
// └── /
|
|
// ├── test (GET)
|
|
// │ • (onRequest) "anonymous()"
|
|
// │ ├── /hello (GET)
|
|
// │ └── ing (GET)
|
|
// │ └── /:param (GET)
|
|
// └── update (PUT)
|
|
|
|
console.log(findMyWay.prettyPrint({ commonPrefix: true, includeMeta: true }))
|
|
// └── / (-)
|
|
// ├── test (GET)
|
|
// │ • (onRequest) "anonymous()"
|
|
// │ • (authIDs) [1,2,3]
|
|
// │ └── /hello (GET)
|
|
// │ • (token) "df123-4567"
|
|
// ├── testing (GET)
|
|
// │ └── /:param (GET)
|
|
// └── update (PUT)
|
|
```
|
|
|
|
<a name="routes"></a>
|
|
#### routes
|
|
Return the all routes **registered** at moment, useful for debugging.
|
|
|
|
```js
|
|
const findMyWay = require('find-my-way')()
|
|
|
|
findMyWay.on('GET', '/test', () => {})
|
|
findMyWay.on('GET', '/test/hello', () => {})
|
|
|
|
console.log(findMyWay.routes)
|
|
// Will print
|
|
// [
|
|
// {
|
|
// method: 'GET',
|
|
// path: '/test',
|
|
// opts: {},
|
|
// handler: [Function],
|
|
// store: undefined
|
|
// },
|
|
// {
|
|
// method: 'GET',
|
|
// path: '/test/hello',
|
|
// opts: {},
|
|
// handler: [Function],
|
|
// store: undefined
|
|
// }
|
|
// ]
|
|
```
|
|
|
|
<a name="acknowledgements"></a>
|
|
## Acknowledgements
|
|
|
|
It is inspired by the [echo](https://github.com/labstack/echo) router, some parts have been extracted from [trekjs](https://github.com/trekjs) router.
|
|
|
|
<a name="sponsor"></a>
|
|
#### Past sponsor
|
|
|
|
- [LetzDoIt](http://www.letzdoitapp.com/)
|
|
|
|
<a name="license"></a>
|
|
## License
|
|
**[find-my-way - MIT](https://github.com/delvedor/find-my-way/blob/master/LICENSE)**<br>
|
|
**[trekjs/router - MIT](https://github.com/trekjs/router/blob/master/LICENSE)**
|
|
|
|
Copyright © 2017-2019 Tomas Della Vedova
|