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.
express
can be replaced withloopback
for StrongLoop LoopBackko.ify(express.Router, express.Route)
can be used to supply the constructors manuallyko.ify(null, express.Route)
will skip.use
and.param
patches
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.