Middleware

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).

Middleware in Express

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:

  • Parsing the body of the request
  • Compressing/decompressing requests and responses
  • Producing access logs
  • Managing sessions
  • Managing encrypted cookies
  • Providing Cross-site Request Forgery (CSRF) protection

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.

Middleware as a pattern

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.

Tip

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:

Middleware as a 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:

  • New middleware can be registered by invoking the 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.
  • When new data is received for processing, the registered middleware is invoked in an asynchronous sequential execution flow. Each unit in the pipeline receives the result of the execution of the previous unit as input.
  • Each piece of middleware can decide to stop further processing of the data by simply not invoking its callback or by passing an error to the callback. An error situation usually triggers the execution of another sequence of middleware that is specifically dedicated to handling errors.

There is no strict rule on how the data is processed and propagated in the pipeline. The strategies include:

  • Augmenting the data with additional properties or functions
  • Replacing the data with the result of some kind of processing
  • Maintaining the immutability of the data and always returning fresh copies as result of processing

The right approach depends on the way the Middleware Manager is implemented and on the type of processing carried out by the middleware itself.

Creating a middleware framework for ØMQ

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.

Note

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.

Note

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 Middleware Manager

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:

  1. Creates two empty lists that will contain our middleware functions, one for the inbound messages and another one for the outbound messages.
  2. It immediately starts listening for new messages coming from the socket by attaching a new listener to the '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.

Note

It's important to understand that this convention for organizing the middleware in pairs is not strictly part of the general pattern, but only an implementation detail of our specific example.

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.

Note

For brevity we are not supporting an error middleware pipeline. Normally, when a middleware function propagates an error, another set of middleware specifically dedicated to handling errors is executed. This can be easily implemented using the same technique that we are demonstrating here.

A middleware to support JSON messages

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:

  • The 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 pipeline
  • The outbound 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.

Using the ØMQ middleware framework

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.

The server

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.

Note

For brevity, we did not show the implementation of the zlib middleware, but you can find it in the sample code that is distributed with the book.

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.

The client

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!

Middleware using generators in Koa

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:

Middleware using generators in Koa

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.

Note

If you need a more complete and reliable implementation of rate-limit middleware, you can have a look at the koajs/ratelimit module:

https://github.com/koajs/ratelimit

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.

Note

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset