“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 namednext
.
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.