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.

Server for Node.js

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store