Server’s middleware ♥ Promises
--
This is the journey of creating a simple and powerful server for Node.js.
I decided to step back and change the way middleware works before committing to 1.0.0. It hasn’t been easy, however I think that we can achieve a lot more with ES6+ than with Connect’s middleware and Koa’s middleware is way too complex for Server’s purpose.
Definition
I decided to create a new standard for Server’s middleware:
Synchronous: just a function that gets the context and returns anything.
// Parse the body from the request
const bodyParser = ctx => {
ctx.req.body = parsebody(ctx.req);
};
Asynchronous: a function that gets the context and returns a promise that will be resolved (or rejected) when the processing finishes.
// Using a Promise-aware library, more and more everyday
const auth = ctx => findUser(ctx.req.session.id).then(user => {
ctx.req.user = user;
});// Porting an existing callback-based library
const ported = ctx => new Promise((resolve, reject) => {
doSomethingAsync({}, (err, cb) => {
if (err) reject(err);
resolve(cb);
});
});
This was a “what would the best way for creating middleware with ES6 from scratch?” kind of idea. I still needed to check if this makes sense at all.
Joining middleware
For testing I made a function that joins them. I got bitten with express when I tried to do the same (100% my fault for not learning how it worked better) but I was surprised at how easy it was when using promises.
The objective of join is that this code works as expected:
// my-custom-middleware.js
const join = require('./join');const parseBody = ctx => {
ctx.req.body = bodyparser(ctx.req);
};const auth = ctx => findUser(ctx.req.session.user).then(user => {
ctx.req.user = user;
});// Return a single middleware that chains them
module.exports = join(parseBody, auth);
This join can be defined with just native code:
// join.js
// Pass a group of middleware and return a single one
module.exports = (...mid) => ctx => mid.reduce((prev, next) => { // Pass always the original context; not the returned one
return prev.then(next).then(fake => ctx);// Get it started with the right context
}, Promise.resolve(ctx));
We should totally do
loadware(mid).reduce...
with loadware to flatten the array, but I didn't want to add this dependency for the example.
That’s it. Now you get a single middleware from those two by just using native code and Promise-aware middleware.
Compatibility with Express/Connect
I also made a function to make any of the original express middleware work with server (making it promise-aware). It is quite easy to use:
// compress.js
const compress = require('compression')({ /* opts */ });
const modern = require('./modern');module.exports = modern(compress);
To allow for dynamic options is slightly more complex:
// body-parser.js
const compress = require('compression');
const modern = require('./modern');module.exports = ctx => {
const middleware = compress(ctx.options.compress);
return modern(middleware)(ctx);
};
Problems
The main problem is that a promise chain cannot be stopped in a non-error way so it’s not possible to easily halt the call even when a router has successfully been called.
We are mostly ignoring the return value and I think promises require to be resolved or rejected to avoid memory leaks, so leaving an unsolved promise also doesn’t help.
This leads to the first constrain:
All the routers will be final; only the first router that matches a path will be used and the rest are ignored:
server(
get('/', ctx => { /* called */ }),
get('/', ctx => { /* ignored */ }),
get('/', ctx => { /* ignored */ }),
);
The second but also important problem is handling errors. All of the middleware accepts a single parameter, ctx, so it cannot be analyzed to see which one is supposed to handle errors.
This leads to the second constrain:
Only throw to the middleware chain when there is a truly catastrophic global issue; try to resolve the local issues within your middleware:
server(
ctx => {
// Recoverable error
// NO: throw new Error('No nnn active. Halt it all!');
// Yes:
ctx.log('No nnn active. To use it make sure ...');
},
);
Conclusion
This is still alpha software, but so far every indication is that we can transition to an awesome Promise-aware world while easily bringing all of the ES5-world code. See more about the project in Github.