Skip to main content

Why we didn't choose NestJS

· 7 min read

In late 2019, our plan was to create a set of tooling which accelerated web development and enabled us to rapidly prototype applications. Because I was basing this on Node & TypeScript, I looked everywhere to see what we can reuse from the community. NestJS (version 6) was the only viable candidate, but it was not enough.

Criteria

Our goal was to create a set of tools to move faster when it came to starting new projects and have them aligned with our mission to create the Scalable Prototype. Our criteria was:

  1. Ability to have separated modules to split logic
  2. Interoperability, modules could extend one another
  3. Inversion of Control / Dependency Injection
  4. Type-safety everywhere
  5. Asynchronous & Type-safe Event Management
  6. Easy integration with MongoDB (incl. data validation )
  7. Real-time data solutions
  8. Isomorphism to have the same concepts in Node, Deno, Browser, ReactNative.

NestJS seemed to hit all criteria except #5, #6, #7, #8. (#5 was later added inside Nest@8). Alright, no biggie we said, we'll write our own modules for the things we needed.

However, after a deep analysis, I decided against using NestJS. It was a hard choice to make, because I just gave myself a lot of extra work to do. But now, in hindsight, it was the right choice.

note

This article is valid even now in late 2021, and if I were to make the same decision now, it would be the same.

Module Management

BlueLibs approach is fundamentally different, a Kernel would contain a list of modules instead of having a module tree-like, that is then instantiated via NestFactory.

Inside BlueLibs Core, a Bundle is the equivallent of a Module in Nest.

HTTP-oriented Module Design

// BlueLibsimport { Kernel } from "@bluelibs/core";import { AppBundle } from "./app.bundle";
const kernel = new Kernel({  bundles: [    new HTTPBundle({      port: 3000,    }),    new AppBundle(),  ],});
async function bootstrap() {  await kernel.init();}
// NestJSimport { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
async function bootstrap() {  const app = await NestFactory.create(AppModule);  await app.listen(3000);}
bootstrap();

The fundamental distinction here is that the NestFactory is responsible of creating the server, so to boot your application, you need to have these "custom factories".

In the case of BlueLibs, the HTTPBundle initialises the server, and the other bundles communicate with it to add routes, meaning that the "HTTP Server Layer" is viewed as yet another bundle, not as something outside of it.

This pattern of thinking allows BlueLibs to have instances of our servers that don't even use HTTP, maybe they just run cronjobs, or process message queues, it's part of the module list.

Even here, in the example where NestJS illustrates Queues it starts an HTTP Server too, because that's how the whole thing is designed, it hasn't been designed to be a fully custom solution, it feels like it was designed to support HTTP requests and have additional modules.

Limited Lifecycle

Another big problem that we noticed was regarding the lifecycle of bundles. onModuleInit() and onApplicationBootstrap() from Nest are not enough for propper manipulation, you need another layer, we called it the prepare(), and in BlueLibs, we have the following life cycles:

extend(); // Gives ability to dynamically add other bundles if they don't exist in the kernelvalidate(); // Validates the configuration of the bundle at runtimehook(); // Gives a chance to attach events for other bundles, (before|after x prepare|init)prepare(); // Prepare the container, or other type of preparations you would need if other bundles use your bundle in the init() phaseinit(); // Do init stuffshutdown(); // When closing

Inside the hook() phase a bundle can hook into other bundle's lifecycle (PREPARE, INIT) and do very customised logic.

Lots of Decorators

While exploring this, I got hit by some very old-school PHP5 vibes. I personally consider a functional approach would be a better fit, they filled it with decorators everywhere, from HTTP Controllers to GraphQL's resolvers and queries.

Our approach was to treat controllers much simpler, they're simply delegators, functions which typically sanitize the request, validate it, delegate it to services, and manipulate the response.

routes.ts
import { RouteType } from "@bluelibs/core";
function CheckLoggedIn(container, req, res, next) {  // Custom re-usable logic here.}
export const routes: RouteType[] = [  {    type: "get",    path: "/users",    handler: [      CheckLoggedIn(),      async (container, req, res, next) => {        // Delegate job to a service.      },    ],  },];
class AppBundle extends Bundle {  async init() {    const http = this.container.get(HTTPBundle);    http.addRoutes(routes);  }}

You can try our solution online, make sure to run npm run start:dev.

Treating the routes in a more functional approach rather than decorators, gives you the ability to actually configure and manipulate these routes however you wish.

You are able to dynamically construct routes, dynamically configure decorators. How would such a thing be possible in NestJS's way of doing things:

// Can I dynamically create more "controllers"?@Controller("cats")export class CatsController {  // Can I add custom decorators based on a specific business-logic variable?  @Get()  findAll(): string {    return "This action returns all cats";  }  // Can I dynamically add additional routes based on some configuration variables from the module?}
// ...unfortunately, the answer to those questions was no.

But the decorators don't just stop there, as we can see here, you have them at field level inside a function for things that you can easily get from req, res.

From my perspective this whole design felt a bit too much, as if someone was forced to use decorators and had no better options.

Lack of Security Modules

NestJS has no official support regarding Security, and this felt like another deal-breaker, as if the others above weren't enough. I strongly believe that a security module is crucial to any backend framework.

I knew that in order to build the Security System, I wanted to have something database-agnostic that handled Users, Sessions & Permissions, and allow the developer to have custom persistence layers for it, if for example he chose to work with a different database.

The problem was that I could not reliably find a way to create this solution with the limited lifecycle events of Nest. We needed a more extended approach.

Isomorphism

Nest is designed to work with Node on the backend. BlueLibs Core works on any JS runtime environment, allowing us to keep the same concepts that make software and have them run in Node, Deno, Browser, React Native, Electron Apps, you get the idea.

Not having support or intention for this, is justified. Nest brought Angular modules to the backend, so it was no reason for them to replace it for the frontend.

BlueLibs Core works in any environment and it's only 5KB in size. For the backend, this is irrelevant.

Encapsulation

Another key element is that we want our code base to be hackable, but not in a bad way, hackable in the sense of configurable, overridable. Sometimes, when using a framework, because of the way it's coded, to do something simple is either extremely complicated and sometimes impossible.

NestJS needs you to explicitly state the providers, the imports and then the exports. In our case, you just need to export the class or the token from your module, and gives you, the coder the ability to access it and override it.

This might not seem as a big thing, just re-export all providers you might say, and that's ok, but it's a matter of philosophy and also with BlueLibs you can "hack" any provider and make it behave as you wish.

In our solution, there's a single container, shared across all modules, and since we use classes or tokens as service ids, there is no collision.

Conclusion

We are thankful for NestJS, they were the first to amp the game up, and thanks to it a lot of people have decided to use Node and TypeScript instead of Java or .NET. It brought enterprise code qualities to the JS ecosystem and that makes me very happy.

However, the torch must be carried on, inside BlueLibs we've reimagined how modules should work, we've added built-in database-agnostic Security modules, real-time data solutions, rapid prototyping solutions and we will continue to pursue our vision.