Skip to content

Brings generators, promises and async functions to Express 4.x middleware and route handlers

License

Notifications You must be signed in to change notification settings

ex-machine/express-ko

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Brings generators, promises and async functions to Express 4.x middleware and route handlers.

Build Status Coverage Status

Description

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.

Differences with Koa

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 control flow

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 control flow

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;
}));

Usage

ko wrapper

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.

Patched global router (recommended)

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 with loopback for StrongLoop LoopBack
  • ko.ify(express.Router, express.Route) can be used to supply the constructors manually
  • ko.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) {  });

Patched isolated router (playing safe)

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;

Unpatched 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) {  }));

async/await

Current implementations (TypeScript, Babel, Regenerator) fall back to generator or regular promise-returning functions, so transpiled async functions can be seamlessly used with ko.

Original way

app.all('/foo', (req, res, next) => {
	.then(() => {
		res.send('foo');
		next();
	});
}); 

Suggested way (async/await)

app.all('/foo', async (req, res) => {
	await ;
	return res.send('foo');
}); 

Alternative way (generators)

app.all('/foo', function* (req, res) {
	yield ;
	return res.send('foo');
}); 

Alternative way (promises)

app.all('/foo', (req, res) => .then(() => res.send('foo'))); 

Implicit next()

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.

Original way

app.all('/foo', (req, res, next) => {
	.then((foo) => {
		res.send(foo);
		next();
	});
}); 

Suggested way (generators)

app.all('/foo', function* (req, res) {
	let foo = yield ; 
	return res.send(foo);
}); 

Alternative way (promises)

app.all('/foo', (req, res) => .then((foo) => res.send(foo))); 

Explicit next()

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.

Original way

app.all('/foo', (req, res, next) => {
	next();
}); 

Suggested way (generators)

app.all('/foo', function* () {
	return ko.NEXT;
}); 

Alternative ways (promises)

app.all('/foo', () => Promise.resolve(ko.NEXT)); 
app.all('/foo', (req, res, next) => Promise.resolve(next)); 

Explicit next('route')

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.

Original way

app.all('/foo', (req, res, next) => {
	next('route');
}, ); 

Suggested way (generators)

app.all('/foo', function* () {
	return ko.NEXT_ROUTE;
}, ); 

Alternative way (promises)

app.all('/foo', () => Promise.resolve(ko.NEXT_ROUTE), ); 

Implicit next(<error>)

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.

Original way

app.all('/foo', (req, res, next) => {
	next(new Error);
}); 

Suggested way

See Explicit next(<error>).

Alternative way (generators)

app.all('/foo', function* () {
	return new Error;
}); 

Alternative way (promises)

app.all('/foo', () => Promise.resolve(new Error)); 

Explicit next(<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'.

Original way

app.all('/foo', (req, res, next) => {
	next('error');
}); 

Suggested way (generators)

app.all('/foo', function* () {
	throw new Error('error')
}); 

Alternative ways (promises)

app.all('/foo', () => .then(() => {
	throw new Error('error');
})); 
app.all('/foo', function* () {
	return Promise.reject('error');
}); 

Implicit res.send(<response>)

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.

Original way

app.all('/foo', (req, res) => {
	.then((foo) => {
		res.send(foo);
	});
}); 

Suggested way (generators)

app.all('/foo', function* (req, res) {
	let foo = yield ; 
	return foo;
}); 

Alternative way (promises)

app.all('/foo', (req, res) => .then((foo) => foo)); 

Implicit res.sendStatus(<number>)

A resolution with number value executes res.sendStatus(<number>).

It is the most suitable way of treating res.sendStatus(…) with no next() case.

Original way

app.all('/foo', (req, res) => {
	.then(() => {
		res.sendStatus(200);
	});
}); 

Suggested way (generators)

app.all('/foo', function* (req, res) {
	yield ; 
	return 200;
}); 

Alternative way (promises)

app.all('/foo', (req, res) => .then(() => 200)); 

Examples

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.