expressor v2.1.1
expressor.js
Easy definition of express routes from a folder structure
Current status
API is stable. There are tests covering all features and options.
v2.0.0 contains breaking changes. Please see changelog
What is it for?
Express is a brilliant and simple web framework for node. But most of the time having to define controllers and then manually link them to routes is unnecessary work.
This module allows routes to be defined by placing controllers in files in a folder structure, and then creates express routes for them automatically, based on the folder structure.
Installation
npm install expressor
Basic usage
expressor( app, [path], [options] )
var express = require('express');
var expressor = require('expressor');
var path = require('path');
var app = express();
expressor(app, path.join(__dirname, 'controllers'));
var server = app.listen(3000, function () {
console.log('Server started');
};
If path is omitted, process.cwd() + 'controllers'
will be used. This isn't foolproof, so best to specify the path manually.
With options
expressor(app, '/path/to/controllers', { /* options */ });
or:
expressor(app, {
path: '/path/to/controllers',
/* options */
});
Controller definitions
If you want to define the following routes:
/
/help
/users
/users/new
/users/:id
/users/:id/edit
/users/:id/delete
create the following file structure:
controllers/index.js
controllers/help/index.js
controllers/users/index.js
controllers/users/new.js
controllers/users/view.js
controllers/users/edit.js
controllers/users/delete.js
Controller definitions would be as follows:
controllers/index.js
// creates routing '/'
module.exports = {
get: function (req, res, next) {
res.send('Welcome to my website');
}
};
controllers/help/index.js
// creates routing '/help'
module.exports = {
get: function (req, res, next) {
res.send('Help page');
}
};
controllers/users/index.js
// creates routing '/users'
module.exports = {
get: function (req, res, next) {
// code to print list of users
}
};
controllers/users/new.js
// creates routing '/users/new'
module.exports = {
get: function (req, res, next) {
// code to print form for new user
},
post: function (req, res, next) {
// code to process form submission
}
};
controllers/users/view.js
// creates routing '/users/:id'
module.exports = {
param: 'id',
pathPart: null, // routes to /users/:id rather than /users/:id/view
get: function (req, res, next) {
// code to print details of user
}
};
controllers/users/edit.js
// creates routing '/users/:id/edit'
module.exports = {
parentAction: 'view',
get: function (req, res, next) {
// code to print form for editing user
},
post: function (req, res, next) {
// code to process form submission
}
};
controllers/users/delete.js
// creates routing '/users/:id/delete'
module.exports = {
parentAction: 'view',
get: function (req, res, next) {
// code to print confirmation form for deleting user
},
post: function (req, res, next) {
// code to process form submission
}
};
Routes & actions
Expressor has the concepts of "routes", "actions" and "methods".
- A route is defined by a folder in the folder structure. e.g.
controllers/users/
folder is theusers
route - An action is the controller, defined by a file in the folder structure. e.g.
controllers/users/edit.js
is theedit
action on theusers
route. - A method is defined by a key on the action object e.g.
get
orpost
Route definitions
In addition to defining actions in files, you can also define attributes of the route (i.e. a group of controllers) by placing a file _index.js
in the route folder.
// controllers/users/_index.js
module.exports = {
pathPart: 'accounts'
};
Actions can be defined in this file rather than one controller per file if preferred:
// controllers/users/_index.js
module.exports = {
actions: {
index: {
get: function (req, res, next) { /* ... */ }
},
new: { /* ... */ },
view: { /* ... */ },
edit: { /* ... */ },
delete: { /* ... */ }
}
};
Routing paths
By default routing paths are created as follows:
- Take the path of the parent action (or parent route's index action for indexes)
- Add the route name (e.g.
users
) - Add the action's params e.g.
param: 'id'
adds:id
to the path - Add the action name (e.g.
edit
)
Overriding/customizing the path
The path can be customized in various ways.
Define in action definition
// controllers/users/index.js
module.exports = {
path: '/accounts',
/* rest of action definition */
};
Change pathPart
in route definition
pathPath
by default inherits the route's name (the folder name) and is used in constructing the path.
// controllers/users/_index.js
module.exports = {
pathPart: 'accounts'
};
This achieves the same as the above examples.
Change pathPart
in action definition
pathPath
by default inherits the action's name (the file name) and is used in constructing the path.
// controllers/users/view.js
module.exports = {
pathPart: null
};
Params
To create a route /users/:userId/:profileId
:
// controllers/users/view.js
module.exports = {
param: ['userId', 'profileId']
};
parentAction
Sit this action on top of another action.
If parentAction
begins with '../'
, it sits on top of the named action in the parent route.
e.g. for /users/:userId/permissions
// controllers/users/permissions/index.js
module.exports = {
parentAction: '../view',
get: function(req, res) { /* etc etc */ }
};
Setting parentAction in the above example sits the route on top of the users/view
action rather than the default users/index
. i.e. makes the route path start /users/:userId/
rather than /users/
.
Path tricks
To make github-style routes /:organisation/:repo
:
- Create a route folder
controllers/organisations
- Set
pathPart: null
in organisations route controller (controllers/organisations/_index.js
) - Set
pathPath: null
in organisations view action (controllers/organisations/view.js
) - Set
param: 'organisation'
in organisations view action (controllers/organisations/view.js
) - Create a route folder
controllers/organisations/repos
- Set
pathPart: null
in repos route controller (controllers/organisations/repos/_index.js
) - Set
parentAction: '../view'
in repos index action (controllers/organisations/repos/index.js
) - Set
pathPath: null
in repos view action (controllers/organisations/repos/view.js
) - Set
param: 'repo'
in repos view action (controllers/organisations/repos/view.js
)
The file controllers/organisations/repos/view.js
will then map to '/:organisation/:repo'.
Advanced usage
Options
Options should be passed to expressor(app, options)
or expressor(app, path, options)
.
methods
Set what methods can be used on actions. Defaults to ['get', 'post']
.
expressor(app, path, { methods: [ 'get', 'post', 'put', 'delete' ] });
endSlash
Controls whether to add a trailing /
on the end of routes with empty action pathPart
. Defaults to false
.
Routings with endSlash = false
:
/users
/users/new
/users/:id
/users/:id/edit
/users/:id/delete
With endSlash = true
:
/users/
/users/new
/users/:id/
/users/:id/edit
/users/:id/delete
(NB only /users/
and /users/:id/
are affected)
indexAction
Set what action is the "index" action i.e. the default action of a route. Defaults to 'index'
.
routeFile
Set name of files containing route definitions. Defaults to '_index.js'
.
paramAttribute
Changes attribute of actions that contains param names. Defaults to 'param'
.
logger
If a function is provided as options.logger
, it is called for each route which is attached to express with a message in format 'Attached route: method'.
expressor(app, path, {logger: console.log});
wrapper
A function that wraps all controller method functions. Wrapper is called with (fn, method, action, app)
and should return a function which will be set as the controller method function in place of the original.
e.g. If you want to write your controller functions to return promises rather than use callbacks:
expressor(app, path, {
wrapper: function(fn, method, action, app) {
return function(req, res, next) {
fn(req, res).then(function() {
console.log('Request handled: ' + req.url);
}).catch(function(err) {
next(err);
});
};
}
});
Then an action might be as defined as follows:
// controllers/users/index.js
module.exports = {
get: function(req, res) {
return User.findAll().then(function(users) {
res.json(users);
});
}
};
load
A set of options passed to require-folder-tree which loads the routing files.
Defaults to the following options:
{
filterFiles: /^([^\._].*)\.js$/,
filterFolders: /^([^\._].*)$/
}
i.e. ignores files and folders with file names starting with '.'
or '_'
.
You could, for example, change these options to write routing files in coffeescript.
Hooks
To allow customization, hooks can be set to run on the tree of routes, either before paths of each action are defined or after.
Hooks are defined in options.hooks
. Hook functions are:
treeBeforePath / treeAfterPath
Called with params (tree, app)
where tree
is the complete tree of routes. app
is the express app.
routeBeforePath / routeAfterPath
Called on each route in the entire routing tree, with params (route, app)
.
actionBeforePath / actionAfterPath
Called on each action in the entire routing tree, with params (action, app)
.
Example
This example sets the param
field of each action where param
is defined as true
to 'route nameId', so that routings are defined like /users/:userId/permissions/:permissionId
expressor(app, path, {
hooks: {
actionBeforePath: function(action, app) {
if (action.param === true) {
action.param = inflection.singularize(action.route.name) + 'Id';
}
}
}
});
NB inflection is a package for converting words between singular and plural.
Tests
Use npm test
to run the tests. Use npm run cover
to check coverage.
Changelog
See changelog.md
Issues
If you discover a bug, please raise an issue on Github. https://github.com/overlookmotel/expressor/issues
Contribution
Pull requests are very welcome. Please:
- ensure all tests pass before submitting PR
- add an entry to changelog
- add tests for new features
- document new functionality/API additions in README