Brings generators, promises and async functions to Express 4.x middleware and route handlers.
express-ko unleashes the power of coroutines on Express middleware functions with a touch of Koa (actually, it is a portmanteau of koa and co).
The package allows to use either promise-returning functions or generator functions (they are wrapped with co and may yield promises as well) as middlewares or route callbacks.
express-ko is nonintrusive. It doesn't hurt the behaviour of existing middlewares and route handlers but augments their functionality.
As opposed to Koa, this context (ctx parameter in Koa 2) is not provided for ko-ified function — particularly because Express 4.x heavily relies on function signatures, like error handlers.
Koa may introduce powerful (and also confusing) patterns to the flow of control, like middleware stack bouncing:
app.use(function* (next) {
this.state.fooBar = 'foo';
// `next` is a generator function and can be delegated
yield* next;
this.body = this.state.foobar;
});
app.use(function* () {
this.state.foobar += yield Promise.resolve('bar');
});express-ko brings some syntax sugar to unsweetened callback hell, but middleware stack still works in one direction:
app.use(ko(function* (req, res, next) {
res.locals.foobar = 'foo';
// `next` is a function and shouldn't be delegated
return next;
}));
app.use(ko(function* () {
res.locals.foobar += yield Promise.resolve('bar');
return res.locals.foobar;
}));The wrapper created with ko() converts generator functions to promises via co. It processes resolution/rejection of a promise chain from both regular and generator functions, doing nothing on the functions that don't return promises.
Express router may be patched with ko.ify(express) to replace all handlers supplied to router use, param and HTTP methods with wrapper functions that look for promises and generators.
expresscan be replaced withloopbackfor StrongLoop LoopBackko.ify(express.Router, express.Route)can be used to supply the constructors manuallyko.ify(null, express.Route)will skip.useand.parampatches
No manual wrapping with ko() is necessary.
let express = require('express');
let ko = require('express-ko');
ko.ify(express);
let app = express();
let router = Router();
app.use(router);
app.use(function (req, res, next) { … })
app.param(function* (req, res, next, val) { … });
router.use(function (err, req, res, next) { … });A new instance of express.Router can be required with cache-mangling packages (rewire, etc) and patched.
This technique is applicable to reusable router module that shouldn't affect Express applications that host it.
Requiring entry point with rewire('express').Router may get cached Router module, it is preferable to rewire it directly.
let ko = require('express-ko');
let rewire = require('rewire');
let IsolatedRouter = rewire('express/lib/router');
let IsolatedRoute = rewire('express/lib/router/route');
let Router = ko.ify(IsolatedRouter, IsolatedRoute);
let router = Router();
router.use(function (err, req, res, next) { … });
module.exports = router;Each callback may be wrapped with ko() or left intact.
ko() needs extra true argument for param callbacks to distinguish them from error handlers.
let express = require('express');
let ko = require('express-ko');
let app = express();
let router = express.Router();
app.use(router);
app.use(function (req, res, next) { … });
app.param(ko(function* (req, res, next, id) { … }, true));
router.use(ko(function (err, req, res, next) { … }));Current implementations (TypeScript, Babel, Regenerator) fall back to generator or regular promise-returning functions, so transpiled async functions can be seamlessly used with ko.
app.all('/foo', (req, res, next) => {
….then(() => {
res.send('foo');
next();
});
}); app.all('/foo', async (req, res) => {
await …;
return res.send('foo');
}); app.all('/foo', function* (req, res) {
yield …;
return res.send('foo');
}); app.all('/foo', (req, res) => ….then(() => res.send('foo'))); A resolution with req or res chain value executes next() and proceeds to next handler/middleware.
It is the most suitable way of treating res.send(…); next(); case.
Node.js HTTP methods (.write(), .end()) don't belong to Express API and aren't suitable for chaining; they return boolean and will cause undesirable implicit response.
app.all('/foo', (req, res, next) => {
….then((foo) => {
res.send(foo);
next();
});
}); app.all('/foo', function* (req, res) {
let foo = yield …;
return res.send(foo);
}); app.all('/foo', (req, res) => ….then((foo) => res.send(foo))); A resolution with ko.NEXT constant or next (uncalled) function values executes next() and proceeds to next handler/middleware.
It is the most suitable way of treating the handlers where no req or res are involved.
next() returns undefined, and resolving with it has no adverse effects. This behaviour of Express isn't documented and can be changed without notice.
app.all('/foo', (req, res, next) => {
next();
}); app.all('/foo', function* () {
return ko.NEXT;
}); app.all('/foo', () => Promise.resolve(ko.NEXT)); app.all('/foo', (req, res, next) => Promise.resolve(next)); A resolution with ko.NEXT_ROUTE constant value executes next('route') and proceeds to next route/middleware.
No magic word 'route' (it has got odorous code smell) has to be involved in this case.
app.all('/foo', (req, res, next) => {
next('route');
}, …); app.all('/foo', function* () {
return ko.NEXT_ROUTE;
}, …); app.all('/foo', () => Promise.resolve(ko.NEXT_ROUTE), …); A resolution with Error object value causes promise chain rejection and executes next(<error>).
This behaviour is implemented to prevent the leakage of Error objects to response and shouldn't be intentionally used.
app.all('/foo', (req, res, next) => {
next(new Error);
}); See Explicit next(<error>).
app.all('/foo', function* () {
return new Error;
}); app.all('/foo', () => Promise.resolve(new Error)); A rejection with <error> value executes next(<error>).
It is preferable to throw an object and not a string, to avoid accidental usage of magic word 'route'.
app.all('/foo', (req, res, next) => {
next('error');
}); app.all('/foo', function* () {
throw new Error('error')
}); app.all('/foo', () => ….then(() => {
throw new Error('error');
})); app.all('/foo', function* () {
return Promise.reject('error');
}); A resolution with any value except undefined and number executes res.send(<response>).
It is the most suitable way of treating res.send(…) or res.json(…) with no next() case.
res.send(<number>) is deprecated in favour of res.sendStatus(<number>). See Express 4.x source code and API documentation on how res.send makes decisions on content type.
app.all('/foo', (req, res) => {
….then((foo) => {
res.send(foo);
});
}); app.all('/foo', function* (req, res) {
let foo = yield …;
return foo;
}); app.all('/foo', (req, res) => ….then((foo) => foo)); A resolution with number value executes res.sendStatus(<number>).
It is the most suitable way of treating res.sendStatus(…) with no next() case.
app.all('/foo', (req, res) => {
….then(() => {
res.sendStatus(200);
});
}); app.all('/foo', function* (req, res) {
yield …;
return 200;
}); app.all('/foo', (req, res) => ….then(() => 200)); foobar application illustrates the new syntax for asynchronous handlers and is available in examples folder, along with foobar-vanilla application that features the original syntax for side-by-side comparison.