This simple, declarative URL router provides Express middleware that can be used to associate metadata with a route. In addition, this module allows an incoming request to be matched to a route at the beginning of the request and allows the handling of the request to be deferred to later in the request. This is helpful in many applications, because intermediate middleware can use the metadata associated with the matched route to conditionally apply security checks, tracking, additional debugging, etc.
Internally, this module utilizes the same module used by Express to parse and match URLs—thus providing an easy transition from the builtin Express router to this router. The router also exposes an API that can be used independent of Express to match a path to route.
Let's say that you want to register authentication middleware for an application, but only a few of the routes actually require authentication. One option is to register the route-specific authentication middleware for each route using code similar to the following:
var authMiddleware = require('my-auth-middleware');
app.get('/account' /* Route path */,
authMiddleware({ redirect: true}) /* Route-specific middleware */,
require('./pages/account') /* Handler: function(req, res, next) { ... } */);
While the above code will work as expected, it has a few drawbacks. The extra route-specific middleware adds clutter and the resulting code is not declarative.
To solve these problems, let's move our routes to a JSON file:
[
{
"route": "GET /account => ./pages/account",
"security": {
"authenticationRequired": true,
"redirect": true
}
}
]
By itself, the "security" metadata for the declared route will have no impact. To enforce authentication we can change the implementation of my-auth-middleware
to be similar to the following:
module.exports = function(req, res, next) {
var routeConfig = req.route && req.route.config;
if (routeConfig.security && routeConfig.security.authenticationRequired) {
if (isUserAuthenticated(req)) {
next();
} else {
// Handle un-authenticated user...
}
} else {
// Route has no security policy... just continue on...
next();
}
}
Finally, to tie everything together we need to register the following middleware (order matters):
// Match the incoming request to a route:
app.use(require('meta-router/middleware').match("/path/to/routes.json"));
// Apply security (if applicable)
app.use(require('my-auth-middleware'));
// Invoke the route handler (if applicable)
app.use(require('meta-router/middleware').invokeHandler());
npm install meta-router --save
routes.json:
[
"GET /users/:user => ./user",
"GET /login => ./user#login",
"GET /logout => ./user#logout",
"POST /users/:user/picture => ./user#uploadPicture"
]
Each route should be of the following format:
<HTTP_METHOD> <PATH> => <HANDLER_MODULE_PATH[#<HANDLER_METHOD>]>
- The
HTTP_METHOD
should beGET
,POST
, etc. - The
PATH
should be an Express-style route path - The
HANDLER_MODULE_PATH
should be the relative path to a Node.js JavaScript module - The optional
HANDLER_METHOD
should be the name of a method on the handler module
The code to register the meta-router
middleware is shown below:
// Match the incoming request to a route:
app.use(require('meta-router/middleware').match("/path/to/routes.json"));
// Intermediate middleware:
app.use(require('my-auth-middleware'));
// Finally, invoke the route handler (if applicable)
app.use(require('meta-router/middleware').invokeHandler());
Route metadata can either be inlined in the JSON routes file or it can be attached to the handler as shown below:
Option 1) Route metadata in JSON file:
[
{
"route": "GET /users/:user => ./user",
"security": {
"authenticationRequired": true,
"redirect": true
}
},
...
]
Option 2) Attach route metadata to handler function:
./user.js
function userHandler(req, res, next) {
// ...
}
userHandler.routeMeta = {
security: {
authenticationRequired: true,
redirect: true
}
}
// You can also optionally attach middleware to the route:
userHandler.routeMiddleware = [
function (req, res, next) {
// ...
},
// ...
]
module.exports = userHandler;
When using a route handler method (e.g. ./user#login
), the JavaScript code to export the login
handler method will be similar to the following:
./user.js
exports.login = function (req, res, next) {
// ...
};
exports.login.routeMeta = { /* ... */ }; // Optional route metadata
exports.login.routeMiddleware = { /* ... */ }; // Optional route-specific middleware
If you don't like JSON Files, you can also choose to configure the routes in your JavaScript code that registers the match
middleware as shown below:
// Match the incoming request to one of the provided routes
app.use(require('meta-router/middleware').match([
{
path: 'GET /users/:user',
middleware: [ // Any number of middleware functions to use for this route (called right before handler)
function(req, res, next) {
if (isNotLoggedIn(req)) {
res.status(401).end('Not authorized');
} else {
next();
}
}
],
handler: function(req, res) {
res.end('Hello user: ' + req.params.user);
},
foo: 'bar' // <-- Arbitrary metadata
},
{
path: 'POST /users/:user/picture',
handler: function(req, res) {
saveProfilePicture(req);
res.end('User profile picture updated!');
}
}
]);
// Invoke the route handler (if available) and end the response:
app.use(require('meta-router/middleware').invokeHandler());
After the match
middleware runs, the matched route information is stored in the req.route
property. The information associated with matched route can be read as shown below:
// Use information from the matched route
app.use(function(req, res, next) {
var route = req.route;
if (route) {
console.log('Route params: ', route.params); // e.g. { user: 'John' }
console.log('Route path: ', route.path); // e.g. "/users/123"
console.log('Route path: ', route.config.path); // e.g. "/users/:user"
console.log('Route methods: ', route.config.methods); // e.g. ['GET']
console.log('Route meta: ', route.config.foo); // e.g. "bar"
}
next();
});
A special matchOptions
property can be included with route metadata to control how the path matching regular expression is generated using path-to-regexp as shown below:
[
{
"route": "GET /users/:user => ./user",
"security": {
"authenticationRequired": true,
"redirect": true
},
"matchOptions": {
"sensitive": false,
"strict": false,
"end": true
}
},
...
]
Supported properties for matchOptions
:
- sensitive When
true
the route will be case sensitive. (default:false
) - strict When
false
the trailing slash is optional. (default:false
) - end When
false
the path will match at the beginning. (default:true
)
The match()
middleware matches an incoming request to one of the possible routes. If an incoming request matched any routes passed in then the req.route
property will be populated with information about the route.
require('meta-router/middleware').match(routes);
The routes
argument can either be an Array
of routes or a path to a JSON routes file (explained later). The format of the routes Array
is explained by the following example below:
[
{
path: 'GET /users/:user', // HTTP method and path
handler: function(req, res) { // Route handler function
...
}, // Route handler function
middleware: [ // Route-specific middleware to run right before the handler
function foo(req, res, next) {
// ...
next();
},
function bar(req, res, next) {
// ...
next();
}
],
// Any additional metadata to associate with this route: (optional)
foo: 'bar',
},
// Alternatively, multiple HTTP methods can be matched
{
methods: ['GET', 'POST']
path: '/foo', // HTTP method and path
handler: ...,
...
},
// Or to match all HTTP methods, the method can be omitted altogether
{
path: '/bar', // HTTP method and path
handler: ...,
...
},
// Additional routes:
...
]
If a String
is passed to the match()
middleware function then it is treated as a path to a JSON routes file. For example:
app.use(require('meta-router/middleware').match(require.resolve('./routes.json'));
Then in ./routes.json
:
[
{
"route": "GET /users/:user => ./path/to/user/handler/module", // HTTP method, path and handler
"middleware": [ // Route-specific middleware to run right before the handler (optional)
"require:foo", // Path to a module that exports a route handler function
{
"factory": "require:bar",
"method": "baz" // Optional name of a property name to lookup the actual factory
"arguments": [ // Optional arguments to use when calling the factory function
"hello",
"world"
]
}
],
// Any additional metadata to associate with this route (optional):
"foo": "bar"
},
...
]
A few things to note when using a JSON routes file:
- JavaScript comments are allowed in the JSON configuration file (they are stripped out before parsing)
- shortstop is used to preprocess the loaded JSON file to resolve handler functions and middleware. All of the handlers provided by shortstop-handlers are registered.
- The
require:
handler supports resolving to a module method using the following syntax:"require:./some-module#someMethod"
The invokeHandler()
can be used to invoke a route handler associated with the matched route.
app.use(require('meta-router/middleware').invokeHandler());
If a route with a handler was matched, then the associated handler function will be invoked (passing in the req
and res
objects). The route handler is expected to end the response to complete the request. If no route was matched or if the route does not have an associated handler then the next middleware in the chain will be invoked.
Given an Array of routes, the buildMatcher(routes)
method will return an object with a match(path[, method])
method that can be used to match a path or path+method to one of the provided routes.
Usage:
var matcher = require('meta-router').buildMatcher([
{
path: 'GET /users/:user',
handler: function(req, res) {
...
}
},
...
]);
var match = matcher.match('/users/123', 'GET');
// match.params.user === '123'
// match.config.path === '/users/:user'
Since the method
argument is optional, the following is also supported:
var matcher = require('meta-router').buildMatcher([
{
path: '/users/:user',
handler: function(req, res) {
...
}
},
...
]);
var match = matcher.match('/users/123');
// match.params.user === '123'
// match.config.path === '/users/:user'
Returns summary information for the routes matchable by meta-router. Returned information is of the form:
[
{"path": "path of the route",
"methods": ["list of methods on the route"]}
]
For example,
[
{
"path":"/keywords",
"methods":[ "GET" ]
},
{
"path":"/update",
"methods":[ "GET", "POST" ]
},
{
"path":"/",
"methods":[ "GET" ]
}
]
The meta-router/macro
can be used to load the routes config at compile time using a babel macro.
const loadRoutes = require("meta-router/macro");
app.use(require('meta-router/middleware').match(loadRoutes("./path/to/routes.json")));
- Add support for
beforeHandler
andafterHandler
functions for each route
See CHANGELOG.md
- Patrick Steele-Idem (Github: @patrick-steele-idem) (Twitter: @psteeleidem)
Pull requests, bug reports and feature requests welcome. To run tests:
npm install
npm test
ISC