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

Journey to launching *server* for Node.js

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

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