One of the most distinctive patterns in Node.js is definitely middleware. Unfortunately, it's also one of the most confusing for the inexperienced, especially for developers coming from the enterprise programming world. The reason for the disorientation is probably connected to the meaning of the term middleware, which in enterprise architecture jargon represents the various software suites that help to abstract lower-level mechanisms such as OS APIs, network communications, memory management, and so on, allowing the developer to focus only on the business case of the application. In this context, the term middleware recalls topics such as CORBA, Enterprise Service Bus, Spring, JBoss, but in its more generic meaning it can also define any kind of software layer that acts like a glue between lower-level services and the application (literally the software in the middle).
Express (http://expressjs.com) popularized the term middleware in the Node.js world, binding it to a very specific design pattern. In Express, in fact, middleware represents a set of services, typically functions, that are organized in a pipeline and are responsible for processing incoming HTTP requests and relative responses.
Express is famous for being a very non-opinionated and minimalist web framework. Using the middleware pattern is an effective strategy for allowing developers to easily create and distribute new features that can be easily added to the current application, without the need to grow the minimalistic core of the framework.
An Express middleware has the following signature:
function(req, res, next) { ... }
Here, req
is the incoming HTTP request, res
is the response, and next
is the callback to be invoked when the current middleware has completed its tasks and that in turn triggers the next middleware in the pipeline.
Examples of the tasks carried out by Express middleware include the following:
If we think about it, these are all tasks that are not strictly related to the main functionality of an application, nor essential parts of a minimal core of a web server; rather, they are accessories, components providing support to the rest of the application and allowing the actual request handlers to focus only on their main business logic. Essentially, those tasks are software in the middle.
The technique used to implement middleware in Express is not new; in fact, it can be considered the Node.js incarnation of the Intercepting Filter pattern and the Chain of Responsibility pattern. In more generic terms, it also represents a processing pipeline, which reminds us about streams. Today, in Node.js the word middleware is used well beyond the boundaries of the Express framework, and indicates a particular pattern whereby a set of processing units, filters, and handlers, under the form of functions, are connected to form an asynchronous sequence in order to perform the preprocessing and postprocessing of any kind of data. The main advantage of this pattern is flexibility; in fact, this pattern allows us to obtain a plugin infrastructure with incredibly little effort, providing an unobtrusive way for extending a system with new filters and handlers.
If you want to know more about the Intercepting Filter pattern, the following article is a good starting point: http://www.oracle.com/technetwork/java/interceptingfilter-142169.html. A nice overview of the Chain of Responsibility pattern is available at this URL: http://java.dzone.com/articles/design-patterns-uncovered-chain-of-responsibility.
The following diagram shows the components of the middleware pattern:
The essential component of the pattern is the Middleware Manager, which is responsible for organizing and executing the middleware functions. The most important implementation details of the pattern are as follows:
use()
function (the name of this function is a common convention in many implementations of this pattern, but we can choose any name). Usually, new middleware can only be appended at the end of the pipeline, but this is not a strict rule.There is no strict rule on how the data is processed and propagated in the pipeline. The strategies include:
The right approach depends on the way the Middleware Manager is implemented and on the type of processing carried out by the middleware itself.
Let's now demonstrate the pattern by building a middleware framework around the ØMQ (http://zeromq.org) messaging library. ØMQ (also known as ZMQ, or ZeroMQ) provides a simple interface for exchanging atomic messages across the network using a variety of protocols; it shines for its performance, and its basic set of abstractions are specifically built to facilitate the implementation of custom messaging architectures. For this reason, ØMQ is often chosen to build complex distributed systems.
In Chapter 11, Messaging and Integration Patterns, we will have the chance to analyze the features of ØMQ in more detail.
The interface of ØMQ is pretty low-level; it only allows us to use strings and binary buffers for messages, so any encoding or custom formatting of data has to be implemented by the users of the library.
In the next example, we are going to build a middleware infrastructure to abstract the preprocessing and post processing of the data passing through a ØMQ socket, so that we can transparently work with JSON objects, but also seamlessly compress messages traveling over the wire.
Before continuing with the example, please make sure to install the ØMQ native libraries following the instructions at this URL: http://zeromq.org/intro:get-the-software. Any version in the 4.0 branch should be enough to work on this example.
The first step toward building a middleware infrastructure around ØMQ is to create a component that is responsible for executing the middleware pipeline when a new message is received or sent. For this purpose, let's create a new module called zmqMiddlewareManager.js
and let's define it:
module.exports = class ZmqMiddlewareManager { constructor(socket) { this.socket = socket; this.inboundMiddleware = []; //[1] this.outboundMiddleware = []; socket.on('message', message => { //[2] this.executeMiddleware(this.inboundMiddleware, { data: message }); }); } send(data) { constmessage = { data: data }; this.executeMiddleware(this.outboundMiddleware, message, () => { this.socket.send(message.data); } ); } use(middleware) { if (middleware.inbound) { this.inboundMiddleware.push(middleware.inbound); } if (middleware.outbound) { this.outboundMiddleware.unshift(middleware.outbound); } } executeMiddleware(middleware, arg, finish) { function iterator(index) { if (index === middleware.length) { return finish && finish(); } middleware[index].call(this, arg, err => { if (err) { return console.log('There was an error: ' + err.message); } iterator.call(this, ++index); }); } iterator.call(this, 0); } };
In the first part of the class, we define the constructor for this new component. It accepts a ØMQ socket as an argument and:
'message'
event. In the listener, we process the inbound message by executing the inboundMiddleware
pipeline.The next method of the ZmqMiddlewareManager
class, send
, is responsible for executing the middleware when a new message is sent through the socket.
This time the message is processed using the filters in the outboundMiddleware
list and then passed to socket.send()
for the actual network transmission.
Now, let's talk about the use
method. This method is necessary for appending new middleware functions to our pipelines. Each middleware comes in pairs; in our implementation it's an object that contains two properties, inbound
and outbound
, that contain the middleware functions to be added to the respective list.
It's important to observe here that the inbound
middleware is pushed to the end of the inboundMiddleware
list, while the outbound
middleware is inserted (using unshift
) at the beginning of the outboundMiddleware
list. This is because complementary inbound/outbound middleware functions usually need to be executed in an inverted order. For example, if we want to decompress and then deserialize an inbound message using JSON, it means that for the outbound, we should instead first serialize and then compress.
The last function, executeMiddleware
, represents the core of our component, it's the function that is responsible for executing the middleware functions. The code from this function should look very familiar; in fact, it is a simple implementation of the asynchronous sequential iteration pattern that we learned in Chapter 3, Asynchronous Control Flow Patterns with Callbacks. Each function in the middleware
array received as input is executed one after the other, and the same arg
object is provided as an argument to each middleware function; this is the trick that makes it possible to propagate the data from one middleware to the next. At the end of the iteration, the finish()
callback is invoked.
Now that we have implemented our Middleware Manager, we can create a pair of middleware functions to demonstrate how to process inbound and outbound messages. As we said, one of the goals of our middleware infrastructure is to have a filter that serializes and deserializes JSON messages, so let's create new middleware to take care of this. In a new module called jsonMiddleware.js
, let's include the following code:
module.exports.json = () => { return { inbound: function (message, next) { message.data = JSON.parse(message.data.toString()); next(); }, outbound: function (message, next) { message.data = new Buffer(JSON.stringify(message.data)); next(); } } };
The json
middleware that we just created is very simple:
inbound
middleware deserializes the message received as an input and assigns the result back to the data
property of message
, so that it can be further processed along the pipelineoutbound
middleware serializes any data found into message.data
Please note how the middleware supported by our framework is quite different from the one used in Express; this is totally normal and a perfect demonstration of how we can adapt this pattern to fit our specific need.
We are now ready to use the middleware infrastructure that we just created. To do that, we are going to build a very simple application, with a client sending a ping to a server at regular intervals and the server echoing back the message received.
From an implementation perspective, we are going to rely on a request/reply messaging pattern using the req/rep socket pair provided by ØMQ (http://zguide.zeromq.org/page:all#Ask-and-Ye-Shall-Receive). We will then wrap the sockets with our zmqMiddlewareManager
to get all the advantages from the middleware infrastructure that we built, including the middleware for serializing/deserializing JSON messages.
Let's start by creating the server side (server.js
). In the first part of the module, we initialize our components:
const zmq = require('zmq'); const ZmqMiddlewareManager = require('./zmqMiddlewareManager'); const jsonMiddleware = require('./jsonMiddleware'); const reply = zmq.socket('rep'); reply.bind('tcp://127.0.0.1:5000');
In the preceding code, we loaded the required dependencies and bind a ØMQ'rep'
(reply) socket to a local port. Next, we initialize our middleware:
const zmqm = new ZmqMiddlewareManager(reply); zmqm.use(jsonMiddleware.json());
We created a new ZmqMiddlewareManager
object and then added two items of middleware, one for compressing/decompressing the messages and another one for parsing/serializing JSON messages.
Now we are ready to handle a request coming from the client; we will do this by simply adding more middleware, this time using it as a request handler:
zmqm.use({ inbound: function (message, next) { console.log('Received: ', message.data); if (message.data.action === 'ping') { this.send({action: 'pong', echo: message.data.echo}); } next(); } });
Since this last item of middleware is defined after the zlib
and json
middleware, we can transparently use the decompressed and deserialized message that is available in the message.data
variable. On the other hand, any data passed to send()
will be processed by the outbound middleware, which in our case will serialize then compress the data.
On the client side of our little application, 'client.js'
, we will first have to initiate a new ØMQ'req'
(request) socket connected to the port 5000
, the one used by our server:
const zmq = require('zmq'); const ZmqMiddlewareManager = require('./zmqMiddlewareManager'); const jsonMiddleware = require('./jsonMiddleware'); const request = zmq.socket('req'); request.connect('tcp://127.0.0.1:5000');
Then, we need to set up our middleware framework in the same way that we did for the server:
const zmqm = new ZmqMiddlewareManager(request); zmqm.use(jsonMiddleware.json());
Next, we create an inbound item of middleware to handle the responses coming from the server:
zmqm.use({ inbound: function (message, next) { console.log('Echoed back: ', message.data); next(); } });
In the preceding code, we simply intercept any inbound response and print it to the console.
Finally, we set up a timer to send some ping requests at regular intervals, always using the zmqMiddlewareManager
to get all the advantages of our middleware:
setInterval( () => { zmqm.send({action: 'ping', echo: Date.now()}); }, 1000);
Note that we are defining all our inbound
and outbound
functions explicitly using the function
keyword, avoiding the usage of the arrow function syntax. This is intentional because, as we learned in Chapter 1, Welcome to the Node.js Platform, the arrow function declaration blocks the function scope to its lexical scope. Using call
on a function defined with an arrow function will not alter its internal scope. In other words, if we use an arrow function, our middleware will not recognize this
as an instance of zmqMiddlewareManager
and a "TypeError: this.send is not a function"
will be raised.
We can now try our application by first starting the server:
node server
We can then start the client with the following command:
node client
At this point, we should see the client sending messages and the server echoing them back.
Our middleware framework did its job; it allowed us to decompress/compress and deserialize/serialize our messages transparently, leaving the handlers free to focus on their business logic!
In the previous paragraphs we saw how it's possible to implement the middleware pattern with callbacks and an example applied to a messaging system.
As we saw when we introduced it, the middleware pattern really shines in web frameworks as a handy mechanism to build "layers" of logic that can deal with input and output as data flows throughout the core of the application.
Apart from Express, another web framework which makes heavy use of the middleware pattern is Koa (http://koajs.com/). Koa is an extremely interesting framework mostly because of its radical choice to implement the middleware pattern only using ES2015 generator functions rather than using callbacks. We will see in a moment how this choice dramatically simplifies the way middleware can be written, but before moving to some code let's picture another way to visualize the middleware pattern, specific for this web framework:
In this representation, we have an incoming request that, before getting into the core of our app, traverses a number of middlewares. This part of the flow is called inbound or downstream. After the flow reaches the core of the app, it traverses all the middlewares again, but this time in an inverse order. This allows the middlewares to perform other actions after the main logic of the app has been executed and the response is ready to be sent to the user. This part of the flow is called outbound or upstream.
The representation above is sometime called "the onion" among programmers because of the way middleware wraps the core app, which reminds us of the layers of an onion.
Now let's create a new web application with Koa to see how we can easily write a custom middleware with generator functions.
Our app will be a very simple JSON API that returns the current timestamp in our server.
First of all, we need to install Koa:
npm install koa
Then we can write our new app.js
:
const app = require('koa')(); app.use(function *(){ this.body = {"now": new Date()}; }); app.listen(3000);
It's important to notice that the core of our application is defined with a generator function within an app.use
call. We will see in a moment that middlewares are added to the app in the exact same way and we will realize that the core of our app is, nonetheless, the last middleware added to the application (and which doesn't need to yield to another following item of middleware).
The first draft of our app is ready. We can now run it:
node app.js
We then point our browser to http://localhost:3000
to see it in action.
Notice that Koa takes care of converting the response to a JSON string and adds the correct content-type
header when we set a JavaScript object to be the body of the current response.
Our API works great, but now we might decide that we would like to protect it from people abusing it and make sure people don't make more than one request in less than one second. This logic can be considered external to the business logic of our API, so we should add it by simply writing a new dedicated item of middleware. Let's write it as a separate module called rateLimit.js
:
const lastCall = new Map(); module.exports = function *(next) { // inbound const now = new Date(); if (lastCall.has(this.ip) && now.getTime() - lastCall.get(this.ip).getTime() < 1000) { return this.status = 429; // Too Many Requests } yield next; // outbound lastCall.set(this.ip, now); this.set('X-RateLimit-Reset', now.getTime() + 1000); };
Our module exports a generator function that implements the logic for our middleware.
The first thing to notice is that we are using a Map
object to store the time we received the last call from a given IP address. We will use this Map
as a sort of in-memory database to be able to check whether a specific user is overloading our server with more than one request per second. Of course this implementation is just a dummy example and it's not ideal in a real case scenario, where it is better to just use external storage such as Redis or Memcache and a more refined logic to detect overloads.
We can see that the body of the middleware is divided in two logical parts, inbound and outbound, separated from the yield next
call. In the inbound part, we haven't hit the core of the application yet, so it's the place where we need to check whether the user is exceeding our rate limit. If that is the case, we simply set the HTTP status code of the response to 429 (too many requests) and we return to stop the execution of the flow.
Another way we can progress to the next item of middleware is by calling yield next
. This is where the magic happens: using generator functions and yield, the execution of the middleware is suspended to execute all the other middlewares in the list sequentially and only when the last item of middleware is executed (the real core of the app) the outbound flow can start and control is given back to every middleware in an inverted order, until the first middleware is called again.
When our middleware again receives control and the generator function is resumed, we take care of storing the timestamp for the successful call, and adding a X-RateLimit-Reset
header to the request, to signal when the user will be able to make a new request.
To enable this middleware, we need to add the following line in our app.js
before the existing app.use
which contains the core logic of our app:
app.use(require('./rateLimit'));
Now to see our new app in action, we need to restart our server and open our browser again. If we refresh the page quickly a few times, we will probably hit the rate limit and we should see the descriptive error message "Too Many Requests". This message is automatically added by Koa as a consequence of setting the status code to 429
and having an empty response body.
If you are interested in reading the actual implementation of the middleware pattern, based on generators used within the Koa framework, you can have a look at the koajs/compose
repository (https://github.com/koajs/compose), which is the core module used to transform an array of generators into a new generator which executes the original ones in a pipeline.