Taking apart NestJS middleware

Taking apart NestJS middleware

Getting into how middleware works in NestJS to gain a deeper understanding

2022-04-24

“What I cannot create, I do not understand” - Richard Feynman

If you’re a backend dev, you’ve doubtless used and written your fair share of middleware. When you call an external middleware to add JSON-support to incoming requests, or implement your own simple middleware to handle authentication, do you ever want to see behind the magic?

I mean, what does a middleware actually do, if we look at the NestJS source code? How does it make things happen?
Oh, by the way, you don’t need to be an expert to follow along….

Prerequisite knowledge: JavaScript, Nodejs

One more thing to clear up before we start: what is middleware?

Middleware is a function which is called before the route handler. Middleware functions have access to the request and response objects, and the next() middleware function in the application’s request-response cycle. The next middleware function is commonly denoted by a variable named next.

Middleware flow

For example, we can implement a middleware to ban users from a certain regions by implementing a middleware that checks the IP before responding to a request:

// this is just express, why? it will make sense in the next section :)
const express = require('express');
var ipLocation = require('ip-location');
const is_ip_private = require('private-ip');
const app = express()

app.use((req, res, next) => {
	// ban North America (just random, sorry Ameribros!)
	const bannedCountries = ['US', 'CA', 'MX'];
	// if this request is forwarded by a proxy like Nginx
	// this won't work, and we'd have to use x-forwarded-for
	const reqIpAddress = req.ip;
	const forwardedIpHeader = req.headers['x-forwarded-for'];
	let realIp;
	
	if (reqIpAddress && !is_ip_private(reqIpAddress)) {
		realIp = reqIpAddress;
	} else if (forwardedIpHeader && !is_ip_private(forwardedIpHeader)) {
		realIp = forwardedIpHeader;
	} else {
		// We couldn't get the real IP address,
		// give up and let the request go ahead.
		next();
	}
	ipLocation(realIp)
		.then(({ country_code }) => {
			if (bannedCountries.includes(country_code)) {
				res.status(403)
				res.send('request refused - disallowed region')
			} else {
				// IP is okay, move along
				next();
			}
		})
		.catch(err => {
			console.log(err);
			next();
		})
});

// ... your app here

This is just off the top of my head - it’s not good enough for a production app, but you can see clearly the role of having this code in the middle between users and the actual routes of your API.

If you still feel iffy on middleware and want a chance to know them better for diving into this article, I highly recommend the NestJS docs on the topic: [Documentation | NestJS - A progressive Node.js framework]( https://docs.nestjs.com/middleware

Now the fun part. How do NestJS middlewares really work? And I don’t mean abstractly, I mean I want to see the literal code and grok it. So let’s pop open Github and see what we’ve got.

How the source code for middleware works

Let’s start by looking at an actual NestJS middleware, using the example from https://docs.nestjs.com/middleware:


import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...');
    next();
  }
}

But before you dive straight into the code hunting for where the middleware logic is deployed, let me save you time by reading the docs:

Nest middleware are, by default, equivalent to express middleware.

Right, Nest doesn’t actually directly implement middleware. It relies on Express for that (or FastAPI, if you configure it to, but most people don’t). This is great news, because Express has a really easy codebase to jump in and understand.

When you add a middleware via app.use(), here’s the code that handles that: https://github.com/expressjs/express/blob/947b6b7d57939d1a3b33ce008765f9aba3eb6f70/lib/router/index.js#L433-L481

Removing the boilerplate for readability, we end up with this:

proto.use = function use(fn) {
	var layer = new Layer(path, {
		sensitive: this.caseSensitive,
		strict: false,
		end: false
	}, fn);
	
	layer.route = undefined;
	this.stack.push(layer);
};

So we create a stack of all the middlewares, which is a wierd choice since we don’t call the last middleware first (stacks are supposed to be Last In - First Out). But whatever! Maybe I’m wrong. Let’s see how the middlewares get called:

Layer.prototype.handle_request = function handle(req, res, next) {
	var fn = this.handle;
	try {
		fn(req, res, next);
	} catch (err) {
		next(err);
	}
};

The actual code is here: express/layer.js at 947b6b7d57939d1a3b33ce008765f9aba3eb6f70 · expressjs/express · GitHub

Okay, but where is this handle_request method being called? When do we actually go through the stack of layers, calling their respective middleware functions? We do that here: https://github.com/expressjs/express/blob/947b6b7d57939d1a3b33ce008765f9aba3eb6f70/lib/router/index.js#L218

// (heavily) abridged code
// The functions are chained together with a series of calls to next()
function next(err) {
// no more matching layers
	if (idx >= stack.length) {
		setImmediate(done, layerError);
		return;
	}

	// find next matching layer
	while (match !== true && idx < stack.length) {
		layer = stack[idx++];
	}

	// ...

	return layer.handle_request(req, res, next);
}

Feel free to look up the other methods called and try to look up what they do. It’s a great exercise for the mind and spirit. Mess around with stuff and modify features. You might not understand everything right away, but you’ll build the right intuitions quickly.

Playing around

But is this even true? Maybe I’ve totally misunderstood! To verify our understanding, I’m going to add console logs to print everytime I add a middleware, and call it, and see if it matches with reality.

First, in the code where we add the layers of middleware to that stack, I’ll add the line console.log(adding middleware ${fn.name}). And I’ll use a simple hello world app to test my changes to Express’s middleware (and by extension, Nest’s as well). So here’s where the app code is from: https://expressjs.com/en/starter/hello-world.html. And I’ll add middlewares like so:

const express = require('./my-express')
const app = express()
const port = 3000

const middlewareOne = () => {
    // does nothing...
}

const middlewareTwo = () => {
    // also does nothing
}

app.use(middlewareOne)
app.use(middlewareTwo)

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

Let’s see what happens when we start it up:

$ node index.js
adding middleware query
adding middleware expressInit
adding middleware middlewareOne
adding middleware middlewareTwo
Example app listening on port 3000

Oh that’s so cool. So in addition to adding our middlewares in the correct order, it appears Express has it’s own “behind the scenes” middlewares going on. Well, hypothesis confirmed.

Doing something

So what can we do that goes beyond normal middleware by incorporating our newfound understanding of the inner workings of middleware? Well, we could make a scanner that shuts down the server if any unapproved of middlewares are detected!

The layers live in the router, in the app object, at app._router.stack. I figured that out by console logging app and looking through this:

<ref *1> [Function: app] {
  ...
  settings: {
    'x-powered-by': true,
    etag: 'weak',
    'etag fn': [Function: generateETag],
    env: 'development',
    'query parser': 'extended',
    'query parser fn': [Function: parseExtendedQueryString],
    'subdomain offset': 2,
    'trust proxy': false,
    'trust proxy fn': [Function: trustNone],
    view: [Function: View],
    views: '/Users/eliana/Documents/Code/wierd/views',
    'jsonp callback name': 'callback'
  },
  locals: [Object: null prototype] {
    settings: {
      'x-powered-by': true,
      etag: 'weak',
      'etag fn': [Function: generateETag],
      env: 'development',
      'query parser': 'extended',
      'query parser fn': [Function: parseExtendedQueryString],
      'subdomain offset': 2,
      'trust proxy': false,
      'trust proxy fn': [Function: trustNone],
      view: [Function: View],
      views: '/Users/eliana/Documents/Code/wierd/views',
      'jsonp callback name': 'callback'
    }
  },
  mountpath: '/',
  _router: [Function: router] {
    params: {},
    _params: [],
    caseSensitive: false,
    mergeParams: undefined,
    strict: false,
    stack: [ [Layer], [Layer], [Layer], [Layer], [Layer], [Layer] ]
  }
}

That line near the end shows us that app._router.stack should have our middlewares. Okay, so let’s try shutting down if any middleware with unknown names are running!

const scanMiddleware = (req, res, next) => {
    approvedMiddleware = ['middlewareOne', 'middlewareTwo', 'scanMiddleware',
        'query', 'expressInit', 'bound dispatch']; // default express middlewares
    const unapprovedMiddleware = app._router.stack
        .some(layer => !approvedMiddleware.includes(layer.name))
    if (unapprovedMiddleware) {
        console.log('unknown middleware detected, shutting down NOW')
        process.exit(1);
    }
    next();
}

app.use(scanMiddleware);

Let’s add an unknown middleware, like app.use(()=>{}) // anonymous middleware... and see how our scanner reacts when we send it a request:

$ node index.js
Now adding the middleware named: query
Now adding the middleware named: expressInit
Now adding the middleware named: scanMiddleware
Now adding the middleware named: middlewareOne
Now adding the middleware named: middlewareTwo
Example app listening on port 3000
An unknown middleware has been detected, shutting down the API NOW
$ # Woohoo, it really worked!!

If we run it without the added anonymous middleware, everything runs just fine. Pretty cool! Kinda makes me wanna transition from my role in security to something more focused on backend. The code is always cleaner on the other stack!

Conclusion

Nest is built atop Express by default (or FastAPI, if you’re adventurous). And Express in turn just adds some basic functionality on top of Nodejs. It’s easy to become intimidated by the complexity and craftsmanship of the ecosystem of modern software, but at the end of the day, it’s just code.

I hope this article goes on to inspire you peek at the source code of other features in software libraries you use. It’s a huge confidence booster figuring out how and why things work the way they do.

By the way, stay tuned for part two, where we explore how NestJS uses FastAPI for middleware! Since it’s newer, FastAPI has a number of innovations and additional sophistication in its approach. If you liked this, you’ll love FastAPI’s approach even more.