Basic Middleware Pattern in JavaScript

Munif Tanjim
Oct 4 · 3 min read
Image for post
Image for post
Photo by Kevin Ku on Unsplash

Ever 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 stuffs
next()
}

In Koa, we have this:

const middleware = (ctx, next) => {
// do stuffs
next()
}

Basically, you have some objects ( , for Express or for Koa) and a function as the arguments of the middleware function. When 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 Koa

app.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 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 = middlewares

const push = (...middlewares) => {
stack.push(...middlewares)
}

const execute = async (context) => {
let prevIndex = -1

const runner = async (index) => {
if (index === prevIndex) {
throw new Error('next() called multiple times')
}

prevIndex = index

const 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 package.

Usage

Let’s see an example of using it:

// create a middleware pipeline
const pipeline = Pipeline(
// with an initial middleware
(ctx, next) => {
console.log(ctx)
next()
}
)

// add some more middlewares
pipeline.push(
(ctx, next) => {
ctx.value = ctx.value + 21
next()
},
(ctx, next) => {
ctx.value = ctx.value * 2
next()
}
)

// add the terminating middleware
pipeline.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> | void

type Middleware<T> = (context: T, next: Next) => Promise<void> | void

type Pipeline<T> = {
push: (...middlewares: Middleware<T>[]) => void
execute: (context: T) => Promise<void>
}

function Pipeline<T>(...middlewares: Middleware<T>[]): Pipeline<T> {
const stack: Middleware<T>[] = middlewares

const push: Pipeline<T>['push'] = (...middlewares) => {
stack.push(...middlewares)
}

const execute: Pipeline<T>['execute'] = async (context) => {
let prevIndex = -1

const runner = async (index: number): Promise<void> => {
if (index === prevIndex) {
throw new Error('next() called multiple times')
}

prevIndex = index

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

Enjoyed this article? If so, get more similar content by subscribing to Decoded, our YouTube channel!

Originally published at https://muniftanjim.dev on October 4, 2020.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Munif Tanjim

Written by

Full Stack Software Engineer with years of professional experience with JavaScript ecosystem. On my free time, I maintain a few open-source libraries and tools.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Munif Tanjim

Written by

Full Stack Software Engineer with years of professional experience with JavaScript ecosystem. On my free time, I maintain a few open-source libraries and tools.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store