Introduction

Node.js has revolutionized how we build servers and web applications. Express.js, being one of the earliest and most popular frameworks in Node.js, helped set the stage. Its middleware pattern, a dual-object decorator with a Continuous Passing Style (CPS) mechanism, was adopted as the go-to approach. Fast-forward to today, a newer framework called Fastify introduces a different pattern - plugins - challenging the status quo. This blog post will delve into the comparison between these two patterns, focusing on their strengths and weaknesses.

The Rise of Middleware Pattern

In the early days of Node.js, the middleware pattern was embraced due to its simplicity and minimalist design. Developers familiar with Node's core HTTP could understand and implement this pattern effortlessly. Moreover, the asynchronous, per-request processing facilitated by the middleware pattern was ideal, given the lack of an asynchronously loadable module system at that time. Node.js's "batteries not included" approach, keeping its core functionalities minimalistic, led to a massively distributed ecosystem growth, solidifying the middleware pattern as the dominant architecture.

Middleware Drawbacks

Despite its initial popularity, over the years, the middleware pattern has shown some limitations:

  1. Shared State and Decorator Pattern: The middleware pattern allows req and res objects to be decorated, effectively sharing their state across the application. As your codebase grows and you consume more libraries, the chances of namespace collisions increase. Moreover, middleware lacks safeguards against such collisions, limiting code reuse across projects.
  2. Order of Execution: Middleware executes in the order of registration. Middleware dependencies need a mechanism for declaring or verifying their peer dependencies within the framework, leading to issues with maintainability and performance.
  3. Synchronous Initialization: Middleware requires synchronous initialization, which breaks encapsulation and leads to leaky abstractions, especially for middleware that needs asynchronous initialization, like database queries.
  4. Incompatibility with Modern Syntax: Promises weren't standardized when the middleware pattern was conceived, and async/await wasn't even conceived. The pattern's incompatibility with modern syntax results in potential memory leaks and challenging bugs.

The Advent of Fastify Plugins

Fastify's unit of abstraction is the plugin, an async function passed the application instance and an options object. This plugin pattern solves many issues of the middleware pattern:

  1. Namespace Collisions: Fastify plugins do not decorate native req/res objects. Instead, they add namespaces at initialization time, avoiding runtime collisions. Fastify also validates namespaces on initialization, failing fast when collisions occur.
  2. Asynchronous Phases: Fastify plugins have two asynchronous phases - init and operation. This deterministic initialization allows plugins to declare peer dependencies, leading to more predictable behaviour and better maintainability.
  3. Encapsulation of Async Initialization: Fastify's asynchronous plugin system fully encapsulates async initialization of a plugin dependency, solving a significant issue with middleware.
  4. Support for Modern Syntax: Created post-ES6, Fastify supports async/await syntax, simplifying the implementation of routes and other functionalities.

A Deeper Look at Initialization

Middleware's synchronous initialization leads to issues with middleware requiring async initialization. This can lead to tight coupling with middleware, boilerplate code, and technical debt. Fastify, on the other hand, uses an intuitive asynchronous initialization pattern. This reduces the chances of bugs, prevents the violation of the Principle of Least Surprise, and avoids the need for extensive knowledge of complex computer science topics.

For a deeper read on this, check out: