Taking apart NestJS middleware Part II: Fastify

Taking apart NestJS middleware Part II: Fastify

Using Fastify middleware with NestJS

2022-05-01

In my previous article, we looked at what a NestJS middleware is actually made of. We saw that by default, NestJS doesn’t actually handle middleware itself:

Nest middleware are, by default, equivalent to express middleware

But Express is just one of two options - NestJS can also run on top of Fastify. What is the difference between the two, from a NestJS dev perspective? From the NestJS performance docs:

Fastify provides a good alternative framework for Nest because it solves design issues in a similar manner to Express. However, fastify is much faster than Express, achieving almost two times better benchmarks results. A fair question is why does Nest use Express as the default HTTP provider? The reason is that Express is widely-used, well-known, and has an enormous set of compatible middleware, which is available to Nest users out-of-the-box.

So Express is more popular, but Fastify has better performance. And running Nest on Fastify is as easy including the FastifyAdapter adapter:

import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter()
  );
  await app.listen(3000);
}
bootstrap();

You’ll also have to install the adapter via NPM with npm i --save @nestjs/platform-fastify.

Middleware is very simple - it’s just an array of functions that access and/or modify the request and response on their way to being routed by your server. Logging, adding headers, and implementing authentication are common use cases for middleware.

Nest middleware flow

We can implement the simplest middleware imaginable by merely console logging something every time a request comes in.

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();
  }
}

Great, so that’s Nest middleware from the perspective of someone merely using Nest with Fastify. But what’s going on under the covers?

We know that Fastify is doing the real work, but how is Fastify getting it done? Let’s take a look under the hood…

How does Fastify (not) handle middleware

Starting with Fastify v3.0.0, middleware is not supported out of the box and requires an external plugin such as fastify-express or middie .

Fastify does provide a mechanism for calling middleware:

await fastify.register(require('middie'))
fastify.use(require('cors')())

Ah, so easy breezy. But that elegance comes at a price - it just outsources the heavy lifting to another library.

The two most popular are Middie and Express. We’re going to cover Middie because it’s a middleware engine built for the sole purpose of serving Fastify.
Plus the previous article completely covers doing this in Express, so Middie is fresh and exciting.

I can’t wait to see how Middie’s approach to middleware compares to Express’s simple yet easy to understand code!

So how does Middie do it?

Middie is so simple, I’m almost surprised they don’t just absorb it into Fastify. Pretty much the entirety of the logic lives in this one 137-line file: middie/engine.js at master · fastify/middie · GitHub.

I recommend you try your best to grok it, because it’s written in a very confusing way. Did you take a look? Good, time to interpret it together!

So the middie function itself gives us two methods - use(), which adds a middleware to Fastify (remember fastify.use(require('cors')()) from the previous section), and run(), which calls all of the middleware that have been added.

// fn is the function passwd
fn(req, res, that.done)

Let’s first look at how middlewares are added. I’ve abridged the code for readability below, but also feel free to consult the original:

  const middlewares = [];
  function use (url, f) {
    middlewares.push({
      fn: f
    });
  }

That’s the gist of what it does, anyway. Read the full code to acquire a more complete understanding: https://github.com/fastify/middie/blob/b2ec14600f44553f10146a6fe3dc8291cb154685/engine.js#L15

Okay, so use() is pretty simple in how it adds middlewares. What about running middleware - how does Middie take care of that for us? Well, as we mentioned before, it’s done via the run() function ( middie/engine.js at b2ec14600f44553f10146a6fe3dc8291cb154685 · fastify/middie · GitHub). I’ve abridged the code to hide some of the noise and complexity so we can see what this function “basically” does, though I strongly encourage you to spend a bunch of time scrolling through the code to try to figure out what every little piece does.

this.i = 0;
while (err || middlewares.length === i) {
  const { fn } = middlewares[i];
  fn(req, res, this.done);
  this.i++;

The code itself is way more convoluted than this, because it is highly micro-optimized. But that’s the basic logic adapted from the file. Now, we’ve seen how middleware are added and call, but how does Middie inject itself into Fastify?

It’s so, so simple. The file index.js takes care of this ( middie/index.js at master · fastify/middie · GitHub). Specifically, these two lines add the use() and run() methods to Fastify:

// Add the .use() method
fastify.decorate('use', use)

// ... lots more code

// Run each middleware successfully
this[kMiddie].run(req.raw, reply.raw, next);
next();

Not bad! For a more complete understanding definitely check out the source code file, it’s only 69 lines including blank lines and comments. But be warned that the code is written in a convoluted way, just like the run() function we saw earlier. Best of luck!

Messing around with Middie

Huh, pretty cool. I wonder how we could use our newfound knowledge to do something interesting or even useful? For sure! Let’s say we wanted to log incoming requests to our servers. No problem, we just create a middleware that logs the req object, right?

await fastify.register(require('middie'));
fastify.use((req, res, next) => {
	console.table(req);
});

Hmm, but what if we want to see what the request looks like at each level of the middleware? In that case, we could modify .run() to log the req object twice (once before it runs and once after). Imagining further, this output could be fed into a cool parser that would then organize and visualize how headers and morphed as they flow through middlewares.

But for now we’ll content ourselves with logging them. Specifically, we’ll just log the value of an imaginary req.whatever attribute, in order to keep our initial code simple.

So first, here is the actual code we’ll run (and then implement in Middie):

await fastify.register(require('modifiedMiddie'));

fastify.use((req, res, next) => {
	req.whatever = "This is the first middleware!";
});

fastify.use((req, res, next) => {
	req.whatever = "This is the second middleware!";
});

So in the run() method we looked at earlier in Middie, we’ll just add the following little snippet:

console.log("Value of req.whatever: " + req.whatever);
fn(req, res, that.done);
console.log("Value of req.whatever: " + req.whatever);

This line, fn(req, res, that.done), occurs twice, so we have to modify both of them. That’s it, let’s run it and see if it really works!

$ node app.js
Value of req.whatever: undefined
Value of req.whatever: undefined
Value of req.whatever: "This is the first middleware!"
Value of req.whatever: "This is the first middleware!"
Value of req.whatever: "This is the second middleware!"
Value of req.whatever: "This is the second middleware!"
Value of req.whatever: "This is the second middleware!"

Since the first and last string are logged to the console more times that we’d expect based on the logic, we can assume that, just like Express, Middie or Fastify are injecting their own middlewares before and after our own.

Easy peasy! If you enjoyed this, I really suggest that you try changing it around for your own purposes and see what crazy things you can come up with. But if nothing else, experiencing the library at this low level will protect you from ever feeling intimidated by NestJS middleware again.

Conclusion

Most people learn a technology like Nest, or a topic like middleware, by simply reading about how to use it and trying it out. I commend this, but think the real learning happens after, when we dig deeper to see what’s going on under the facade. Beneath the elegant and well-documented developer facing APIs are the warts. Quick hacks never intended to see the light of day, that maintainers patch together to get the job done in time for the next release. That’s where I learn, anyway.

Nest is a great tool to learn this way. Hackable, readable source code and easy concepts. The design is modular, so you can replace parts as you wish and see how different libraries interact with it (like Fastify vs Express). Have fun, do something wierd, and as always: Happy Hacking!