API is stable. There are tests covering all features and options.
v2.0.0 contains breaking changes. Please see changelog
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.
npm install expressor
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.
expressor(app, '/path/to/controllers', { /* options */ });
or:
expressor(app, {
path: '/path/to/controllers',
/* options */
});
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:
// creates routing '/'
module.exports = {
get: function (req, res, next) {
res.send('Welcome to my website');
}
};
// creates routing '/help'
module.exports = {
get: function (req, res, next) {
res.send('Help page');
}
};
// creates routing '/users'
module.exports = {
get: function (req, res, next) {
// code to print list of users
}
};
// 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
}
};
// 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
}
};
// 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
}
};
// 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
}
};
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
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: { /* ... */ }
}
};
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
)
The path can be customized in various ways.
// controllers/users/index.js
module.exports = {
path: '/accounts',
/* rest of action 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.
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
};
To create a route /users/:userId/:profileId
:
// controllers/users/view.js
module.exports = {
param: ['userId', 'profileId']
};
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/
.
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'.
Options should be passed to expressor(app, options)
or expressor(app, path, options)
.
Set what methods can be used on actions. Defaults to ['get', 'post']
.
expressor(app, path, { methods: [ 'get', 'post', 'put', 'delete' ] });
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)
Set what action is the "index" action i.e. the default action of a route. Defaults to 'index'
.
Set name of files containing route definitions. Defaults to '_index.js'
.
Changes attribute of actions that contains param names. Defaults to 'param'
.
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] [path]'.
expressor(app, path, {logger: console.log});
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);
});
}
};
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.
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:
Called with params (tree, app)
where tree
is the complete tree of routes. app
is the express app.
Called on each route in the entire routing tree, with params (route, app)
.
Called on each action in the entire routing tree, with params (action, app)
.
This example sets the param
field of each action where param
is defined as true
to '[route name]Id', 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.
Use npm test
to run the tests. Use npm run cover
to check coverage.
See changelog.md
If you discover a bug, please raise an issue on Github. https://github.com/overlookmotel/expressor/issues
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