Middleware

Middleware allows you to intercept, modify, or add behavior to your application's request handling. You attach middleware to Boltzmann either to your application, or to individual handlers. This allows you to modify request handling for all, or a subset of, your handlers.

Middleware is useful for:

Middleware is Boltzmann's primary mechanism for exposing configurable behavior to you. It is also Boltzmann's mechanism for enabling dependency injection. It's a powerful concept! This document will cover how to talk about middleware and how to attach it to your application. Other documents cover how to write middleware and what middleware boltzmann makes available to your application.

:warning: This document does not cover how to parse incoming request bodies. that information is available in the "handling user input" document.


# What is middleware?

Middleware is a repeatable mechanism for layering handlers and allowing them to delegate control to inner handlers. That's kind of a mouthful!

Middleware is structured like an onion. A single middleware is like a layer of the onion. When Boltzmann responds to a request, it draws a line through that onion. Each layer may pass control to the next layer in the onion using next(), or it can respond to the request directly, skipping the inner layers of the onion. For every layer the request passes through coming in, it will have to travel back out through in order to send a response.

If you cut one of the outer layers off of an onion and take out the inside, you get another, smaller onion. This is analogous to Boltzmann middleware: if you cut a layer of middleware off of your application, you still have the other layers making up the application. Boltzmann uses this property to let outer layers of the application assert facts that inner layers may rely upon without knowing how those facts are asserted. Concretely: if you are using redis middleware, every layer of your application past the redis middleware layer may rely on the availability of context.redisClient, but they are agnostic of how that property came to exist and the concrete type to which it points. Indeed, under test, you might cut out the redis middleware and replace it with middleware that puts a mock redis client implementation in place!

In order to talk about how to write middleware, it is handy to have a shared vocabulary for talking about the anatomy of a single middleware. What follows is a TypeScript set of definitions for middleware, and an associated example. Do not fret if you're not comfortable reading this syntax! After the example each of the types will be broken down in natural language.

const HEADER = Symbol.for('headers')
const STATUS = Symbol.for('status')

type HttpMetadata      = {[HEADER]: {[key: string]: string}} & {[STATUS]: number};
type UserResponse      = string | AsyncIterable<Buffer | string> | Buffer | Object;
type BoltzmannResponse = (AsyncIterable<Buffer | string> | Buffer | Object) & HttpMetadata;
type Handler           = (context: Context, ...args: any[]) => UserResponse | Promise<UserResponse>;
type Next              = (context: Context, ...args: any[]) => Promise<BoltzmannResponse>;
type Adaptor           = (next: Next) => Handler | Promise<Handler>;
type Middleware        = (options?: Object) => Adaptor;

function middleware (_options = {}) {
  return function adaptor (next: Next): Handler {
    return async function handler (context: Context, ...args): Promise<UserResponse> {
      return next(context, ...args)
    }
  }
}

Middleware has three parts: the outermost function receives configuration options, which it is responsible for validating. It may throw an error if bad options are provided, preventing the application from starting. Middleware is generally called once at application startup. Naming this function is useful: the name of the middleware will be included in Honeycomb traces if that feature is enabled, and it will be displayed by the development-mode debugging middleware if a stall happens.

Middleware returns an Adaptor function. The Adaptor function receives a Next function as an argument and returns a Handler. Adaptors, like the outer middleware, are called once at application startup. The Adaptor can be asynchronous! If there are asynchronous setup steps, like connecting to a database or reading a file from disk, they should be performed here. Again, this function may throw an error if configuration is bad or necessary resources are unavailable.

The application will not start accepting requests until the Adaptor returns a Handler. The Handler is executed whenever your application receives an HTTP request; it receives a Context object and returns a UserResponse. It may call the Next function provided as an argument to the Adaptor function zero-to-many times. You can think of calling next() as making a request to the inner part of your application! next() will never throw, and the return value is guaranteed to have a status code and headers associated with it. The body of the Handler is where your middleware logic should be implemented.


# Attaching & Configuring Middleware

Middleware may be attached in one of two places, which is referred to as the "scope" of the middleware. The widest scope is application-wide: if you need logic to be performed on every request sent to your application, you can attach it in middleware.js or middleware/index.js by exporting an APP_MIDDLEWARE property that points at an array of your middleware:

// middleware/index.js
const myMiddleware = require('./my-middleware')

module.exports = {
  APP_MIDDLEWARE: [
    myMiddleware
  ]
}

This will register the middleware application-wide. Application-scope attached middleware will execute before Boltzmann routes the request; they will execute even for requests that match no corresponding route in your application!

If, on the other hand, you need logic to be applied to many handlers, but not all, you may wish to attach your middleware directly to handlers:

// handlers.js

module.exports = {
  palindromesOnly,
  anyWords
}

anyWords.route = 'GET /any/:utterance'
function anyWords (context, { utterance }) {
  return 'yep that seems about correct'
}

const handleNonPalindromes = require('../middleware/non-palindromes')
palindromesOnly.route = 'GET /palindromes/:utterance'
palindromesOnly.middleware = [
  handleNonPalindromes // highly sophisticated middleware that 404s on non-palindromes
]
function palindromesOnly (context, { utterance }) {
  return 'ok ko'
}

In this example, only palindromesOnly is guarded by the handleNonPalindromes middleware.


Both app-wide and handler-specific middleware scopes accept an Array of Middleware. Middleware is executed in order, and responses from inner middleware bubble back up the list in reverse order:

// middleware.js
module.exports = {
  APP_MIDDLEWARE: [
    one,
    two,
    three
  ]
}
// handlers.js
module.exports = { hello }

hello.route = 'GET /'
hello.middleware = [
  four,
  five
]
function hello (context) {
  return 'six'
}

In this example, a request to GET / will execute one, two, three, four, five, and finally respond with six. The 'six' response will be received by five, then four, then three, and so on.

If a given middleware requires additional configuration, it can be provided arguments by turning the entry into an array of [Middleware, ...arguments], which will be evaluated when the application starts up:

// middleware.js
module.exports = {
  APP_MIDDLEWARE: [
    one,
    [two, {wow: 'an argument'}], // configure "two" with additional params
    three
  ]
}
// handlers.js
module.exports = { hello }

hello.route = 'GET /'
hello.middleware = [
  [four, 'argument one', 'argument two'],
  five
]
function hello (context) {
  return 'six'
}

Insofar as is possible, middleware should be designed such that attaching it with no arguments is valid. If configuration is necessary, it should fail noisily at startup!

# Next Steps

Now that you know the vocabulary for middleware, and how and when to attach it to your application, it's time to write some middleware. [This guide][ref-guide] will take you through writing your first middleware. You may also be interested in reviewing the built-in middleware that Boltzmann makes available. Happy hacking!