Middleware Brigade is a library for composing and calling middleware. Middleware is a pattern for using a series of semi-independent modules to accomplish a goal or compute a result. Each module is called a middleware and has a standard function signature and receives data in a standard format. The middleware then processes the data and passes control to the next middleware in the chain.
Middleware Brigade is named after the concept of a Bucket Brigade
A brigade is an ordered list of middleware functions, usually an Array
.
A middleware function is any function conforming to the Middleware Brigade function signature and contract.
A composed brigade is a middleware function that calls each middleware function in a brigade in series and coordinates
communications between them as well as error handling.
Middleware Brigade is a library for producing and calling composed brigades.
A Middleware Brigade middleware function is always asynchronous. If javascript async
functions are used, it has the following signature:
async function middleware(request, next, terminate) {
}
or, alternatively, a Promise
object can be explicitly returned from a standard synchronous JavaScript function:
function middleware(request, next, terminate) {
return Promise.reject(new Error('Failure'));
}
request
is an object that represents the data that the middleware function operates on.
next
and terminate
are functions that determine how the rest of the middleware chain is processed.
These are known as continue functions. The middleware function MUST call either next
or terminate
, or throw an error
or return a rejected promise.
if next
is called, then control is passed to the next middleware in the brigade, which will receive the
same request
object.
async function middleware(request, next, terminate) {
result = await next();
// slightly redundant for clarity.
return result;
}
if terminate
is called, the middleware function will be considered the end of the brigade and
no further middleware will be called in this brigade.
Both next
and terminate
are asynchronous functions and their return values must be appropriately processed.
In the prior example, an async
function, await
can be used to wait for the return value, which should
be returned from the middleware.
If the Promise
form is used, the promise returned by next
or terminate
can be directly used or returned.
function middleware(request, next, terminate) {
return next().then(result => { /* stuff */ return result; });
}
Most middleware functions will call next
, so declaring the terminate
parameter is optional.
function middleware(request, next) {
return next().then(result => { /* stuff */ return result; });
}
Each middleware function must return either a rejected promise indicating an error or a promise that resolves to the
result of the brigade. Usually, the result is simply received from calling next
or terminate
,
modified, and then returned. This is very much like a bucket brigade.
await
is not required if the result of calling next
is returned directly. This is acceptable:
function middleware(request, next) {
return next();
}
or
async function middleware(request, next) {
return next();
}
An array of middleware functions can be composed into a single middleware function known as a composed brigade. A a composed brigade has the middleware function signature and can itself be composed into another composed brigade, or called directly.
import compose from 'middleware-brigade'
const myBrigade = compose([
async (context, next) => {
// Do something
const result = await next();
// Do something else
return result;
},
async (context, next) => {
// Do something
const result = await next();
// Do something else
return result;
},
]);
When middleware functions are composed into a composed brigade, error handling is included in the resulting middleware
function. It can make sense to use compose
to wrap even a a single middleware function to obtain that error handling.
Calling compose
with an empty list will create a middleware function that simply
calls its next
function.
There are three conventions in Middleware Brigade for calling a middleware function. Two for starting a composed brigade, and one for choosing alternatives while executing a composed brigade.
The middleware pattern is often used to organize complex request/response computations. When both the request and response can be complex, It makes sense to have objects represent both the request and the response. Many times, the future response object is created at the same time as the request object, such as in node's http module.
In these cases, the Sentinel Response form can be used to begin a Brigade.
import { callMiddleware, compose } from 'middleware-brigade'
function createHandler(middleware) {
const brigade = compose(middleware);
return handler;
function handler(request, response) {
return callMiddleware(brigade, request, response);
}
}
The function callMiddleware
will accept a pre-existing request and response object and begin calling each
middleware in the chain, creating next
and terminate
functions that properly continue control and return
the passed response object.
The advantage to this form is that since the response object is known in advance when the call chain is complete a check can be made to ensure that the chain returns that exact object. An error is raised if it does not. This can detect many types of asynchronous failure modes.
In this form, every middleware in the brigade receives the exact same request object and response object.
If you do not care what the end result of the brigade is, and also do not have a natural response object
for the sentinel response form, its recommended that still use the sentinel response form and pass an empty object as the sentinel.
callMiddleware(brigade, request, {})
If the response is not known in advance, but is being calculated by the brigade, callMiddleware
should
be used without passing a response object.
import { callMiddleware, compose } from 'middleware-brigade'
function createHandler(middleware) {
const brigade = compose(middleware);
return handler;
function handler(request) {
return callMiddleware(brigade, request);
}
}
Middleware Brigade will check the end result of the brigade and if it is undefined
, an error will be raised.
Some failure modes cannot be detected using this form.
There are times when evaluating a brigade when it useful to make a decision and continue evaluating
different brigades based on some condition. For example, in routing middleware. Since this case is continuing
an already executing brigade and not beginning a new brigade, the calling form is slightly different. Instead of
calling callMiddleware
, just directly call a composed middleware representing the brigade. Using compose
is recommended because
of the error handling it adds.
import { callMiddleware, compose } from 'middleware-brigade'
function createChoice(A, B) {
const brigadeA = compose(A);
const brigadeB = compose(B)
return MiddlewareWithAlternatives;
function MiddlewareWithAlternatives(request, next, terminate) {
if (request.condition) {
return brigadeA(request, next, terminate);
} else {
return brigadeB(request, next, terminate);
}
}
}
In async
middleware functions, errors from calling the continue function will be
registered as exceptions. JavaScript will automatically convert rejected promises to
exceptions. The errors can be handled with try
and catch
.
async function middleware(request, next, terminate) {
try {
const result = await next();
} catch (e) {
handleError(e);
}
return result;
}
Errors can be raised by using throw
.
async function middleware(request, next, terminate) {
throw new Error('Always Fails');
}
In a standard function returning a promise, the catch
method should be used.
async function middleware(request, next, terminate) {
return next().catch( err => handleError(e));
}
Returns a rejected promise to raise an error.
async function middleware(request, next, terminate) {
return Promise.reject('Always Fails');
}
Middleware Brigade middleware is asynchronous and uses Promises to coordinate code that runs at different times.
(Under the hood, JavaScript's async
functions map to promises.) The biggest failure mode with
Middleware Brigade is failing to chain together the promise received from calling next
or terminate
with
the promise returned by the middleware function. Much of the structure of Middlware Brigade is meant to prevent or detect that.
An async
function always returns a promise. Any value that is returned is wrapped in a Promise
.
If no return value is specified, a Promise resolving to undefined
will be returned.
For example, this middleware function has no connection between the Promise returned by next
and the
implicit Promise created by javascript when badExample
finishes executing.
function badExample(request, next, terminate) {
next();
}
This can be re-written as
function goodExample(request, next, terminate) {
return next();
}
This example returns a promise, but does not wait for its result. await
is required to halt execution
until the remainder of the chain is complete. The following example will execute in an
unexpected order. doSomething
may execute before logic specified by next
.
function badExample(request, next, terminate) {
const result = next();
doSomething();
return result;
}
Adding await
makes the execution order deterministic.
function goodExample(request, next, terminate) {
const result = await next();
doSomething();
return result;
}
When writing tests for middleware functions, please remember to write a test case where the continue function
method used ()next
or terminate
) completes with an expected result object, as well as when
it completes with a rejected Promise.
Unlike connect style middleware and like Koa, Middlware Brigade middleware is asynchronous.
A middleware function may allow the chain to continue while continuing to process the request.
Middleware Brigade values robust error detection and helpful error messaging. Many of the features of the middleware function signature were chosen to enable this.
Middleware Brigade includes flowtype type definitions. Types can detect many types of errors, including errors which Middlware Brigade also eplicitly checks for. The advantage to type checking is that the error can be caught earlier, at development time.
Leave feedback on the type definitions
Connect style middleware accepts both a request
and response
parameters and the middleware may modify the response
before continuing to the rest of the middleware chain, or after. Middleware Brigade includes only the request
parameter. The
response is acquired from the rest of the brigade by calling the continue function (next
or terminate
).
This was done for two reasons.
First, if a middleware function modifies the response before it gets passed down the chain, middleware functions later in the brigade might not know what modifications were made, so if something radical needs to be done to the response later, the state of the response might contain a mixed state. By only modifying response from the end of the brigade toward the beginning, it is believed the state will be more stable under more conditions. Maybe. This is an hypothesis and remains to be proven.
Second, forcing the middleware function to acquire the response from calling next
makes it more visible and less likely that
the chain of asynchronous operations will be unexpectedly broken.
Koa middleware uses a single context
parameter that encapsulates both the request and response. This style is also
usable with Middleware Brigade by attaching the response object to the request object. (And possibly vise-versa.)
Are there cases where response is needed prior to calling next?
Its an accepted middleware pattern for a middleware function to determine that it has fully handled a request and refuse to
continue evaluation of the rest of the brigade. In most middleware implementations, the middleware simply
returns without calling next
. In these cases it can be hard to determine if the intent was to not continue, or if
there was a programming error. A comment might be added to indicate that the call to next
was intentionally omitted.
Unlikely other middleware, Middleware Brigade uses passes two different continue functions in its signature, next
and
terminate
. In Middleware Brigade terminate
is called to indicate that the brigade should terminate. This makes the intent
clear to the reader of the code, eliminates the need for a comment, and allows for the detection of the unintentional
failure to pass control down the brigade.
Without the terminate
parameter, the Sentinel Response pattern would not be possible since there
would be no standard way for the middleware function to acquire the sentinel to return it in the case where the brigade
was being prematurely terminated.
Its the intent of Middlware Brigade to support es6 modules. Currently babel is used. I'm not sure what the best pattern is for deploying es6 modules to both node and browser contexts. Help wanted.
What is the best way to use es6 modules with npm?
It is presumed that if the request were simple enough to not be an object, then also the middleware pattern would not be required. The restriction could be loosened. Feedback is welcome.
Every middleware function would have to pass the request object it receives to its continuation function, or create a new request object to pass. This would be a source of errors.
You should re-implement callMiddleware
to better suit your use case. This case was intentionally not supported to make
callMiddleware
simpler.
Middlware Brigade is based on koa-compose and began as a pull request against that library. Koa and Koa-compose are phenonominal works of engineering. However, some goals of Middleware Brigade are not goals of Koa and it did not make sense to merge the PR in the context of Koa's goals, especially when the benefit remains unproven and would possibly involve a traumatic BC break for Koa. I released that work here to be able to use it in other contexts and get a sense of whether there is a value in those choices. I hope whatever proves of value here will make it back to Koa.