Basic Middleware Pattern in JavaScript
Published on byMunif TanjimEver wondered how the middlewares in popular web frameworks, e.g. Express or Koa, work?
In Express, we have the middleware functions with this signature:
const middleare = (req, res, next) => {// do stuffsnext()}
In Koa, we have this:
const middleware = (ctx, next) => {// do stuffsnext()}
Basically, you have some objects (req
, res
for Express or ctx
for Koa) and
a next()
function as the arguments of the middleware function. When next()
is called, the next middleware function is invoked. If you modify the argument
objects in the current middleware function, the next middleware will received
those modified objects. For example:
// Middleware usage in Koaapp.use((ctx, next) => {ctx.name = 'Doe'next()})app.use((ctx, next) => {console.log(ctx.name) // will log `Doe`})app.use((ctx, next) => {// this will not get invoked})
And if you don’t call the next()
function, the execution stops there and the
next middleware function will not be invoked.
Implementation
So, how do you implement a pattern like that? With 30 lines of JavaScript:
function Pipeline(...middlewares) {const stack = middlewaresconst push = (...middlewares) => {stack.push(...middlewares)}const execute = async (context) => {let prevIndex = -1const runner = async (index) => {if (index === prevIndex) {throw new Error('next() called multiple times')}prevIndex = indexconst middleware = stack[index]if (middleware) {await middleware(context, () => {return runner(index + 1)})}}await runner(0)}return { push, execute }}
This implementation of middleware pattern is almost the same as Koa. If you
want to see how Koa does it, check out the source code of
koa-compose
package.
Usage
Let’s see an example of using it:
// create a middleware pipelineconst pipeline = Pipeline(// with an initial middleware(ctx, next) => {console.log(ctx)next()})// add some more middlewarespipeline.push((ctx, next) => {ctx.value = ctx.value + 21next()},(ctx, next) => {ctx.value = ctx.value * 2next()})// add the terminating middlewarepipeline.push((ctx, next) => {console.log(ctx)// not calling `next()`})// add another one for fun ¯\_(ツ)_/¯pipeline.push((ctx, next) => {console.log('this will not be logged')})// execute the pipeline with initial value of `ctx`pipeline.execute({ value: 0 })
If you run that piece of code, can you guess what the output will be? Yeah, you guessed it right:
{ value: 0 }{ value: 42 }
By the way, this would absolutely work with async middleware functions too.
TypeScript
Now, how about giving it some TypeScript love?
type Next = () => Promise<void> | voidtype Middleware<T> = (context: T, next: Next) => Promise<void> | voidtype Pipeline<T> = {push: (...middlewares: Middleware<T>[]) => voidexecute: (context: T) => Promise<void>}function Pipeline<T>(...middlewares: Middleware<T>[]): Pipeline<T> {const stack: Middleware<T>[] = middlewaresconst push: Pipeline<T>['push'] = (...middlewares) => {stack.push(...middlewares)}const execute: Pipeline<T>['execute'] = async (context) => {let prevIndex = -1const runner = async (index: number): Promise<void> => {if (index === prevIndex) {throw new Error('next() called multiple times')}prevIndex = indexconst middleware = stack[index]if (middleware) {await middleware(context, () => {return runner(index + 1)})}}await runner(0)}return { push, execute }}
With everything being typed, now you can declare the type of the context object for a specific middleware pipeline, like this:
type Context = {value: number}const pipeline = Pipeline<Context>()
Okay, that’s all for now.