Skip to content

Commit

Permalink
feat!: Replace find-my-way with @medley/router
Browse files Browse the repository at this point in the history
This enables any HTTP method to be supported (only with HTTP/2, since the http/https modules reject non-standard HTTP methods).
This also adds the `methodNotAllowedHandler` option.

BREAKING CHANGE:
* find-my-way has been removed, so the path formats supported by find-my-way are no longer supported
* The `maxParamLength` and `ignoreTrailingSlash` options have been removed (only strict routing is supported now)
  • Loading branch information
nwoltman committed Oct 17, 2019
1 parent b3798de commit 14083c5
Show file tree
Hide file tree
Showing 15 changed files with 610 additions and 416 deletions.
27 changes: 27 additions & 0 deletions docs/App.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,33 @@ const app = medley();
app.createSubApp('/api').register(require('./routes'));
```

The path in the route definition will be directly appended to the path prefix
passed to `createSubApp()`. There is no special handling of `/` characters
(except for one case described below).

```js
const medley = require('@medley/medley');
const app = medley();

const subApp = app.createSubApp('/user');
subApp.get('/', (req, res) => {}); // Creates a route for '/user/'
// The above route will not run for: '/user'.
// The following would create a route on the sub-app for '/user':
subApp.get('', (req, res) => {});
```

The only case where registering a route on a sub-app with a prefix includes
special handling for the `/` character is when the sub-app’s prefix ends
with a `/` and the route path begins with a `/`.

```js
const medley = require('@medley/medley');
const app = medley();

const subApp = app.createSubApp('/users/');
subApp.get('/:id', (req, res) => {}); // Creates a route for '/users/:id'
```

<a id="decorate"></a>
### `app.decorate(name, value)`

Expand Down
6 changes: 5 additions & 1 deletion docs/Lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Incoming Request

The first step Medley takes after receiving a request is to find the route that matches the URL of the request.

Medley uses the [`find-my-way`](https://www.npmjs.com/package/find-my-way) router to make this step fast and efficient.
Medley uses [`@medley/router`](https://www.npmjs.com/package/@medley/router) for routing.

## `onRequest` Hooks

Expand Down Expand Up @@ -80,6 +80,10 @@ See the [`Routes` documentation](Routes.md) for more information on route handle

If the request URL does not match any routes, the [`notFoundHandler`](Medley.md#notfoundhandler) is invoked. Global hooks **are** run before/after this handler.

#### Method-Not-Allowed Handler

If the request URL matches a route but has no handler for the request method, the [`methodNotAllowedHandler`](Medley.md#methodnotallowedhandler) is invoked. Global hooks **are** run before/after this handler.

## Serialize Body

In this step, the body that was passed to `res.send()` is serialized (if it needs to be) and an appropriate `Content-Type` is set (if one was not already set).
Expand Down
94 changes: 51 additions & 43 deletions docs/Medley.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ object which is used to customize the resulting instance. The options are:

+ [`http2`](#http2)
+ [`https`](#https)
+ [`maxParamLength`](#maxparamlength)
+ [`methodNotAllowedHandler`](#methodnotallowedhandler)
+ [`notFoundHandler`](#notfoundhandler)
+ [`onErrorSending`](#onerrorsending)
+ [`queryParser`](#queryparser)
+ [`server`](#server)
+ [`strictRouting`](#strictrouting)
+ [`trustProxy`](#trustproxy)

## Options
Expand Down Expand Up @@ -52,18 +51,59 @@ const app = medley({
});
```

### `maxParamLength`
### `methodNotAllowedHandler`

Type: `number`<br>
Default: `100`
Type: `function(req, res)` (`req` - [Request](Request.md), `res` - [Response](Response.md))

A handler function that is called when the request URL matches a route but
there is no handler for the request method.

```js
const medley = require('@medley/medley');
const app = medley({
methodNotAllowedHandler: (req, res) => {
res.status(405).send('Method Not Allowed: ' + req.method);
}
});
```

[`405 Method Not Allowed`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405)
responses require the [`Allow`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Allow)
header to be set. To make this convenient, the [`res.config`](Response.md#resconfig)
object in the handler will contain an `allowedMethods` property, which will be an
array of the HTTP methods that have handlers for the route.

```js
const medley = require('@medley/medley');
const app = medley({
methodNotAllowedHandler: (req, res) => {
res.config.allowedMethods; // ['GET', 'HEAD', 'POST']
res.setHeader('allow', res.config.allowedMethods.join());
res.status(405).send('Method Not Allowed: ' + req.method);
}
});

app.get('/users', (req, res) => { /* ... */ });
app.post('/users', (req, res) => { /* ... */ });
```

While this handler is intended to be used to send a [`405 Method Not Allowed`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405)
response, it could be used to send a `404 Not Found` response instead.

```js
const medley = require('@medley/medley');

This option sets a limit on the number of characters in the parameters of
parametric (standard, regex, and multi-parametric) routes.
function notFoundHandler(req, res) {
res.status(404).send('Not Found: ' + req.url);
}

This can be useful to protect against [DoS attacks](https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS)
for routes with regex parameters.
const app = medley({
notFoundHandler,
methodNotAllowedHandler: notFoundHandler,
});
```

*If the maximum length limit is reached, the request will not match the route.*
[Hooks](Hooks.md) that are added to the root `app` will run before/after the `methodNotAllowedHandler`.

### `notFoundHandler`

Expand All @@ -75,7 +115,7 @@ A handler function that is called when no routes match the request URL.
const medley = require('@medley/medley');
const app = medley({
notFoundHandler: (req, res) => {
res.status(404).send('Route Not Found');
res.status(404).send('Not Found: ' + req.url);
}
});
```
Expand Down Expand Up @@ -138,38 +178,6 @@ const server = http.createServer()
const app = medley({ server });
```

### `strictRouting`

Default: `false`

Enables strict routing. When `true`, the router treats "/foo" and "/foo/" as
different. Otherwise, the router treats "/foo" and "/foo/" as the same.

```js
const medley = require('@medley/medley');
const app = medley({ strictRouting: false });

// Registers both "/foo" and "/foo/"
app.get('/foo/', (req, res) => {
res.send('foo');
});

// Registers both "/bar" and "/bar/"
app.get('/bar', (req, res) => {
res.send('bar');
});

const strictApp = medley({ strictRouting: true });

strictApp.get('/foo', (req, res) => {
res.send('foo');
});

strictApp.get('/foo/', (req, res) => {
res.send('different foo');
});
```

### `trustProxy`

Default: `false`
Expand Down
111 changes: 68 additions & 43 deletions docs/Routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ app.route(options)

Type: `string` | `Array<string>`

The HTTP method(s) the route will run for. Can be any method found in the
[`http.METHODS`](https://nodejs.org/api/http.html#http_http_methods) array
(except for `CONNECT`).
The HTTP method(s) the route will run for.

```js
app.route({
Expand Down Expand Up @@ -55,12 +53,12 @@ app.route({
method: 'GET',
path: '/user/:id',
handler: function(req, res) {
console.log(req.params.id); // '1003' (for example)
console.log(req.url); // '/user/1003' (for example)
}
});
````

See the [URL-Building](#url-building) section for details on the formats
See the [**Route Path Formats**](#route-path-formats) section for details on the formats
the `path` option can take.

#### `handler` (required)
Expand Down Expand Up @@ -186,7 +184,7 @@ app.patch(path, [options,] handler)
app.delete(path, [options,] handler)
app.options(path, [options,] handler)

// Registers a route that handles all supported methods
// Registers a route that handles all methods in `require('http').METHODS`
app.all(path, [options,] handler)
```

Expand Down Expand Up @@ -245,64 +243,91 @@ app.get('/path', {
*If the `handler` is specified in both the `options` object and as the
third parameter, the third parameter will take precedence.*

## URL-Building
## Route Path Formats

Medley supports any route paths supported by
[`find-my-way`](https://github.com/delvedor/find-my-way).
Medley supports any route path format supported by [`@medley/router`](https://github.com/@medley/router).

URL parameters are specified with a colon (`:`) before the parameter
name, and wildcard paths use an asterisk (`*`).
The path formats are as follows:

_Note that static routes are always checked before parametric and wildcard routes._
### 1. Static

```js
// Static
app.get('/api/user', (req, res) => {});

// Parametric
app.get('/api/:userId', (req, res) => {});
app.get('/api/:userId/:secretToken', (req, res) => {});
Static routes match exactly the path provided.

// Wildcard
app.get('/api/*', (req, res) => {});
```
/
/about
/api/login
```

Regular expression routes are also supported, but be aware that they are
expensive in terms of performance.
### 2. Parametric

```js
// Parametric with regex
app.get('/api/:file(^\\d+).png', (req, res) => {});
Path segments that begin with a `:` denote a parameter and will match anything
up to the next `/` or to the end of the path.

```
/users/:userID
/users/:userID/posts
/users/:userID/posts/:postID
```

To define a path with more than one parameter within the same path part,
a hyphen (`-`) can be used to separate the parameters:
Everything after the `:` character will be the name of the parameter in the
`req.params` object.

```js
// Multi-parametric
app.get('/api/near/:lat-:lng/radius/:r', (req, res) => {
// Matches: '/api/near/10.856-32.284/radius/50'
req.params // { lat: '10.856', lng: '32.284', r: '50' }
app.get('/users/:userID', (req, res) => {
// Request URL: /users/100
console.log(req.params); // { userID: '100' }
});
```

Multiple parameters also work with regular expressions:
If multiple routes have a parameter in the same part of the route, the
parameter names must be the same. For example, registering the following two
routes would be an error because the `:id` and `:userID` parameters conflict
with each other:

```
/users/:id
/users/:userID/posts
```

Parameters may start anywhere in the path. For example, the following are valid routes:

```js
app.get('/api/at/:hour(^\\d{2})h:minute(^\\d{2})m', (req, res) => {
// Matches: '/api/at/02h:50m'
req.params // { hour: '02', minute: '50' }
});
'/api/v:version' // Matches '/api/v1'
'/on-:event' // Matches '/on-click'
```

### 3. Wildcard

Routes that end with a `*` are wildcard routes. The `*` will match any
characters in the rest of the path, including `/` characters or no characters.

For example, the following route:

```
/static/*
```

In this case, the parameter separator can be any character that is not
matched by the regular expression.
will match all of these URLs:

```
/static/
/static/favicon.ico
/static/js/main.js
/static/css/vendor/bootstrap.css
```

Having a route with multiple parameters may affect negatively the performance,
so prefer the single parameter approach whenever possible.
The wildcard value will be set in the route `params` object with `'*'` as the key.

For more information on the router used by Medley, check out
[`find-my-way`](https://github.com/delvedor/find-my-way).
```js
app.get('/static/*', (req, res) => {
if (req.url === '/static/favicon.ico') {
console.log(req.params); // { '*': 'favicon.ico' }
} else if (req.url === '/static/') {
console.log(req.params); // { '*': '' }
}
});
```

<a id="async-await"></a>
## Async-Await / Promises
Expand Down
Loading

0 comments on commit 14083c5

Please sign in to comment.