Middleware

This document covers the built-in middleware Boltzmann makes available. Some middleware is automatically installed by Boltzmann when scaffolded with certain feature flags. Other middleware you need to attach yourself, either to specific handlers or to your app.

# User-attached middleware

# applyHeaders

To be documented.

# applyXFO

The applyXFO middleware adds an X-Frame-Options header to responses. It accepts one configuration value, the value of the header to set. This value must be one of SAMEORIGIN or DENY.

Example usage:

'use strict'

const boltzmann = require('./boltzmann')

module.exports = {
  APP_MIDDLEWARE: [
    [boltzmann.middleware.applyXFO, 'DENY'],
  ],
}

# authenticateJWT

To be documented.


# esbuild

To be documented.


# handleCORS

The handleCORS middleware is always available to be attached. It configures headers to control Cross-Origin Resource Sharing, or CORS.

Arguments:

Example Usage:

const boltzmann = require('./boltzmann')
const isDev = require('are-we-dev')

module.exports = {
  APP_MIDDLEWARE: [
    [ boltzmann.middleware.handleCORS, {
      origins: isDev() ? '*' : [ 'www.example.com', 'another.example.com' ],
      methods: [ 'GET', 'POST', 'PATCH', 'PUT', 'DELETE' ],
      headers: [ 'Origin', 'Content-Type', 'Accept', 'Accept-Version', 'x-my-custom-header' ],
    } ],
  ],
}

# oauth

Added in 0.2.0.

This feature implements support for using OAuth 2.0 to authenticate a user with an external service provider, such as Google or Auth0. Enabling the feature provides four middlewares:

Arguments:

The OAuth middleware has many configuration knobs and dials to turn, but the middleware is usable in development if you set three environment variables: OAUTH_DOMAIN, OAUTH_CLIENT_SECRET, and OAUTH_CLIENT_ID. If you set those variables, the code required to attach oauth middleware looks like this:

const { middleware } = require('./boltzmann')

// with process.env.{OAUTH_DOMAIN, OAUTH_CLIENT_SECRET, OAUTH_CLIENT_ID} all set
module.exports = {
  APP_MIDDLEWARE: [
    middleware.oauth
  ]
};

Advanced configuration:

If you have a more complex setup, the individual middlewares can be configured differently. In each case, if you do not provide an optional configuration field, the default is determined as documented above.

handleOauthCallback() respects the following configuration fields:

handleOauthLogin() respects the following configuration fields:

handleOauthLogin() respects the following configuration fields:


# route

Added in 0.5.0.

To be documented.


# session

Added in 0.1.4.

You can import session middleware with require('./boltzmann').middleware.session. The session middleware provides HTTP session support using sealed http-only cookies. You can read more about Boltzmann's session support in the "storage" chapter.

Arguments:

Example Usage:

const { middleware } = require('./boltzmann')

// The most basic configuration. Relies on environment variables being set for required values.
// Consider using this!
module.exports = {
  APP_MIDDLEWARE: [
    middleware.session
  ]
};

// A simple configuration, hard-coding the values. Don't actually do this.
module.exports = {
  APP_MIDDLEWARE: [
    [middleware.session, { secret: 'wow a great secret, just amazing'.repeat(2), salt: 'salty' }],
  ]
};

// A complicated example, where you store sessions on the filesystem, because
// the filesystem is a database.
const fs = require('fs').promise
module.exports = {
  APP_MIDDLEWARE: [
    [middleware.session, {
      async save (_context, id, data) {
        // We receive "_context" in case there are any clients we wish to use
        // to save or load our data. In this case, we're using the filesystem,
        // so we can ignore the context.
        return await fs.writeFile(id, 'utf8', JSON.stringify(id))
      },
      async load (_context, id) {
        return JSON.parse(await fs.readFile(id, 'utf8'))
      }
    }]
  ]
}

module.exports = {
  // A configuration that sets "same-site" to "lax", suitable for sites that require cookies
  // to be sent when redirecting from an external site. E.g., sites that use OAuth-style login
  // flows.
  APP_MIDDLEWARE: [
    [middleware.session, { cookieOptions: { sameSite: 'lax' } }],
  ]
};

# staticfiles

To be documented.


# template

The template middleware is available if you have enabled the templating feature with --templates=on. It enables returning rendered nunjucks templates from handlers. See the website features overview for a description of how to use templates to build websites and the development conveniences provided.

Arguments:


# templateContext

The template middleware is available if you have enabled the templating feature with --templates=on. It allows you to add extra data to every context value sent to template rendering.

Arguments:

Example Usage:

const boltzmann = require('./boltzmann')
async function fetchActiveUsers(context) {
  // do something with i/o here
}

module.exports = {
  APP_MIDDLEWARE: [
    [
      boltzmann.middleware.applyCSRF,
      [ boltzmann.middleware.templateContext, {
        siteTitle: 'Boltzmann User Conference',
        activeUsers: fetchActiveUsers
      } ],
      boltzmann.middleware.template,
    ],
  ],
}

# test

To be documented


# validate.body

Added in 0.0.0. Changelog
  • Changed in 0.1.7: Bugfix to support validator use as middleware.
  • Changed in 0.2.0: Added support for schemas defined via [fluent-json-schema].
  • Changed in 0.5.0: Added second options argument, accepting [ajv].

The validate.body middleware applies JSON schema validation to incoming request bodies. It intercepts the body that would be returned by [context.body] and validates it against the given schema, throwing a 400 Bad Request error on validation failure. If the body passes validation it is passed through.

Ajv is configured with {useDefaults: true, allErrors: true} by default. In development mode, strictTypes is set to true. In non-development mode, it is set to "log".

Arguments:

validate.body(schema[, { ajv }])

Example Usage:

// handlers.js
const { middleware } = require('boltzmann')

example.middleware = [
  [middleware.validate.body, {
    type: 'object',
    required: ['id'],
    properties: {
      id: { type: 'string', format: 'uuid' }
    }
  }]
]
example.route = 'POST /example'
export async function example (context) {
  // if body.id isn't a uuid, this throws a 400 Bad request error,
  // otherwise `id` is a string containing a uuid:
  const { id } = await context.body
}

const Ajv = require('ajv')
customAjv.middleware = [
  [middleware.validate.body, {
    type: 'object',
    required: ['id'],
    properties: {
      id: { type: 'string', format: 'uuid' }
    }
  }, {
    // You can customize Ajv behavior by providing your own Ajv
    // instance, like so:
    ajv: new Ajv({
      coerceTypes: true
    })
  }]
]
customAjv.route = 'POST /custom'
export async function customAjv (context) {
  // if body.id isn't a uuid, this throws a 400 Bad request error,
  // otherwise `id` is a string containing a uuid:
  const { id } = await context.body
}

# validate.params

Added in 0.0.0. Changelog
  • Changed in 0.1.7: Bugfix to support validator use as middleware.
  • Changed in 0.2.0: Added support for schemas defined via [fluent-json-schema].
  • Changed in 0.5.0: Added second options argument, accepting ajv.

The validate.params middleware applies JSON schema validation to url parameters matched during request routing. Matched URL parameters are validated against the given schema, throwing a 400 Bad Request error on validation failure, preventing execution of the handler. If the parameters pass validation the handler is called.

Ajv is configured with {allErrors: true, useDefaults: true, coerceTypes: "array"} by default. In development mode, strictTypes is set to true. In non-development mode, it is set to "log".

Arguments:

validate.params(schema[, { ajv }])

Example Usage:

// handlers.js
const { middleware } = require('boltzmann')

example.middleware = [
  [middleware.validate.params, {
    type: 'object',
    required: ['id'],
    properties: {
      id: { type: 'string', format: 'uuid' }
    }
  }]
]
example.route = 'GET /example/:id'
export async function example (context) {
  const { id } = context.params
}

const Ajv = require('ajv')
customAjv.middleware = [
  [middleware.validate.params, {
    type: 'object',
    required: ['id'],
    properties: {
      id: { type: 'string', format: 'uuid' }
    }
  }, {
    // You can customize Ajv behavior by providing your own Ajv
    // instance, like so:
    ajv: new Ajv({
      coerceTypes: true
    })
  }]
]
customAjv.route = 'GET /:id'
export async function customAjv (context) {
  const { id } = context.params
}

# validate.query

Added in 0.0.0. Changelog
  • Changed in 0.1.7: Bugfix to support validator use as middleware.
  • Changed in 0.2.0: Added support for schemas defined via [fluent-json-schema].
  • Changed in 0.5.0: Added second options argument, accepting ajv.

The validate.query middleware applies JSON schema validation to incoming HTTP query (or "search") parameters. Query parameters are validated against the given schema, throwing a 400 Bad Request error on validation failure, preventing execution of the handler. If the query parameters pass validation the handler is called.

Ajv is configured with {allErrors: true, useDefaults: true, coerceTypes: "array"} by default. In development mode, strictTypes is set to true. In non-development mode, it is set to "log".

Arguments:

validate.query(schema[, { ajv }])

Example Usage:

// handlers.js
const { middleware } = require('boltzmann')

example.middleware = [
  [middleware.validate.query, {
    type: 'object',
    required: ['id'],
    properties: {
      id: { type: 'string', format: 'uuid' }
    }
  }]
]
example.route = 'GET /example'
export async function example (context) {
  const { id } = context.query
}

const Ajv = require('ajv')
customAjv.middleware = [
  [middleware.validate.query, {
    type: 'object',
    required: ['id'],
    properties: {
      id: { type: 'string', format: 'uuid' }
    }
  }, {
    // You can customize Ajv behavior by providing your own Ajv
    // instance, like so:
    ajv: new Ajv({
      coerceTypes: true
    })
  }]
]
customAjv.route = 'GET /custom'
export async function customAjv (context) {
  const { id } = context.query
}

# vary

Added in 0.5.0.

The vary middleware unconditionally updates responses to include a Vary header with the configured values. This is useful for handlers that change behavior based on context.cookie. It is automatically installed for handlers that use the .version attribute.

Arguments:

Example Usage:

// handlers.js
const { middleware } = require('./boltzmann.js')
cookies.middleware = [
  [middleware.vary, 'cookie']
]
cookies.route = 'GET /'
export function cookies(context) {
  return context.cookie.get('wow') ? 'great' : 'not great'
}

// multiple values may be set at once.
multi.middleware = [
  [middleware.vary, ['cookie', 'accept-encoding']]
]
multi.route = 'GET /multi'
export function multi(context) {
  return context.cookie.get('wow') ? 'great' : 'not great'
}

# Automatically attached middleware

Automatically-attached middleware is middleware you can configure but do not need to attach to the app yourself. Boltzmann automatically attaches these middlewares if the features that provide them are enabled. You can often configure this middleware, however, using environment variables.

# attachPostgres

This middleware is enabled when the postgres feature is enabled. It creates a postgres client and makes it available on the context object via an async getter. To use it:

const client = await context.postgresClient

Configure the postgres client with these two environment variables:


# attachRedis

This middleware is attached when the redis feature is enabled. It adds a configured, promisified Redis client to the context object accessible via the getter context.redisClient. This object is a handy-redis client with a promisified API. The environment variable REDIS_URL is passed to the handy-redis constructor.


# devMiddleware

This middleware is attached when Boltzmann runs in development mode. It provides stall and hang timers to aid in detecting and debugging slow middleware.

You can configure what slow means in your use case by setting these two environment variables:

This middleware does nothing if your app is not in development mode.


# handlePing

This middleware adds a handler at GET /monitor/ping. It responds with a short text string that is selected randomly at process start. This endpoint is intended to be called often by load balancers or other automated processes that check if the process is listening. No other middleware is invoked for this endpoint. In particular, it is not logged.


# handleStatus

This middleware is attached when the status feature is enabled. It mounts a handler at GET /monitor/status that includes helpful information about the process status and the results of the reachability checks added by the redis and postgres features, if those are also enabled. The response is a single json object, like this one:

{
    "downstream": {
        "redisReachability": {
            "error": null,
            "latency": 1,
            "status": "healthy"
        }
    },
    "hostname": "catnip.local",
    "memory": {
        "arrayBuffers": 58703,
        "external": 1522825,
        "heapTotal": 7008256,
        "heapUsed": 5384288,
        "rss": 29138944
    },
    "service": "hello",
    "stats": {
        "requestCount": 3,
        "statuses": {
            "200": 2,
            "404": 1
        }
    },
    "uptime": 196.845680345
}

This endpoint uses the value of the environment variable GIT_COMMIT, if set, to populate the git field of this response structure. Set this if you find it useful to identify which commit identifies the build a specific process is running.

If you have enabled this endpoint, you might wish to make sure it is not externally accessible. A common way of doing this is to block routes that match /monitor/ in external-facing proxies or load balancers.


# livereload

To be documented.


# log

This middleware is always attached to Boltzmann apps.

This middleware configures the bole logger and enables per-request logging. In development mode, the logger is configured using bistre pretty-printing. In production mode, the output is newline-delimited json.

To configure the log level, set the environment variable LOG_LEVEL to a level that bole supports. The default level is debug. To tag your logs with a specific name, set the environment variable SERVICE_NAME. The default name is boltzmann.

Here is an example of the request logging:

> env SERVICE_NAME=hello NODE_ENV=production ./boltzmann.js
{"time":"2020-11-16T23:28:58.104Z","hostname":"catnip.local","pid":19186,"level":"info","name":"server","message":"now listening on port 5000"}
{"time":"2020-11-16T23:29:02.375Z","hostname":"catnip.local","pid":19186,"level":"info","name":"hello","message":"200 GET /hello/world","id":"GSV Total Internal Reflection","ip":"::1","host":"localhost","method":"GET","url":"/hello/world","elapsed":1,"status":200,"userAgent":"HTTPie/2.3.0"}

The id fields in logs is the value of the request-id, available on the context object as the id field. This is set by examining headers for an existing id. Boltzmann consults x-honeycomb-trace and x-request-id before falling back to generating a request id using a short randomly-selected string.

To log from your handlers, you might write code like this:

const logger = require('bole')('handlers')

async function greeting(/** @type {Context} */ context) {
    logger.info(`extending a hearty welcome to ${context.params.name}`)
    return `hello ${context.params.name}`
}

# route

Added in 0.5.0.

Boltzmann automatically attaches one instance of route.

To be documented.


# trace

This middleware is added to your service if you have enabled the honeycomb feature. This feature sends trace data to the Honeycomb service for deep observability of the performance of your handlers.

To configure this middleware, set the following environment variables:

The sampling rate defaults to 1 if neither sample rate env var is set. Tracing is disabled if a write key and dataset are not provided; the middleware is still attached but does nothing in this case.