Ep.08: Nest.js (Basic app for Node Backend Frameworks Comparison)
Generated with DreamStudio

Ep.08: Nest.js (Basic app for Node Backend Frameworks Comparison)

This article is part of an in-depth comparison series of the top Node frameworks. We'll cover all the important aspects: market share, learning curve, ecosystem, security and more. To compare them properly, we will build the exact same app in all these frameworks (plus Vanilla Node), observe all the steps along the way and then benchmark them as we progressively add more functionalities.

Table of contents

  1. Market Share Distribution Analysis: Picking the most used frameworks
  2. Planning Phase: Use cases, Minimum Viable Product requirements, Architecture
  3. Tools & Setup: Getting started and setting up the tools and environment
  4. Scaffolding: Project Settings and Configuration, Common template repo
  5. Express: basic app (MVP)
  6. Koa: basic app (MVP)
  7. Express Flavors: basic app (MVP) in multiple variants: OOP, FP, TypeScript, single-file etc
  8. Nest.js: basic app (MVP)
  9. Fastify: basic app (MVP)
  10. Next.js: basic app (MVP)
  11. Vanilla Node: basic app (MVP)
  12. Functional testing with Postman + Newman


Alright, this is an entirely different beast. For starters, it's at the opposite end of the spectrum compared to minimalistic frameworks like Express, Koa, Fastify, Restify and Hapi, and, unlike those, it's very feature-rich and opinionated about how your project should look like.

Is that good or bad? Well, I'm going to answer Matrix-style: this is the wrong question to ask. A better one is: what projects is it fit for?

If you're running a large team and a complex project that needs to scale easily, you're going to run into a problem that Nest excels at fixing:

  • maintaining coding standards and patterns, project structure, type safety (Nest is TypeScript enabled);
  • built-in and predictable approach to major aspects like Logging, Exception handling, Validation, Performance monitoring, Configuration and many many more. No more battles for which logging library to use or days spent searching for a compatible library, or replacing that deprecated plugin...;
  • detailed official documentation, which is sorely lacking for most of the millions of libraries available for other frameworks;
  • native support for GraphQL, WebSockets, gRPC, MQTT;
  • includes OpenApi / Swagger and Compodoc support out of the box;
  • comes with its own dev tools, like hot reload (you won't need to install nodemon or pm2, etc), REPL and CLI;
  • extensive ORM support: TypeORM, MikroORM, Prisma, Sequelize, Mongoose;
  • built-in unit testing, e2e testing, assertion and coverage libraries with Jest and Automock;
  • standard 'Recipes' for almost anything you want to achieve. You want to use SWC (Speedy Web Compiler - an extensible Rust-based platform that can be used for both compilation and bundling)? Nest has got you covered. You need to implement Health checks? Coming right up. You want CLI support as well? No problem, it comes with its own CLI and you can even enhance it with your own commands. CQRS (Command and Query Responsibility Segregation)? Yup, got it. And so on...

Nest calls itself a progressive framework. Now what in the name of the Holy Admin is that? Well, it basically means that it:

  • uses modern JavaScript features.
  • integrates proven design patterns.
  • maintains a balance between cutting-edge features and stability.
  • offers a high degree of customization and flexibility.
  • doesn't restrict developers to a single programming paradigm.

Another great feature is that it is built with and fully supports TypeScript (yet still enables developers to code in pure JavaScript).

It follows SOLID principles, and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).

And, to put a cherry on top, it uses Express by default under the hood, while it can be configured to use Fastify as well!

And, last but not least, it comes with its own dependency manager, which basically means that every controller, provider, etc gets instantiated only once, at application start-up, and then that instance gets injected everywhere you need it. that means that if you have many concurrent requests, it will save memory and time by getting the already instantiated instance of every class needed by that request rather than creating new instances each time. Kinda like everything's a singleton by default.

But what if I use serverless or cloud functions, like lambdas, where there's a penalty for cold-starting your app for each function call? No worries, Nest comes with lazy loading options, so you can only load whatever that function needs.

Ok, that's a behemoth of a framework and a lot to take in. Probably it comes with a gazillion dependencies, right? Let's dive in.


Analysis (Nest.js)

All right, this is going to be tougher to compare, as Nest is split across multiple components. So, we will look at the core ones that we'd need to build our app: @nestjs/core, @nestjs/common, but there's plenty more out there.

  • DEPENDENCIES: To be honest, I expected a lot more, but there are only 18 for core and 4 for common. But the ones in 'common' are also present in 'core', so, it is 18 in fact. That is incredibly light! Also, just 6 modules with a lone maintainer, but Nest has healthy financing and corporate backing, so even those are not worrisome.
  • VULNERABILITIES: looking good, npm audit: 0, Snyk gives it a healthy 91/100 score with no known security issues, Socket scans it in the green.
  • SIZE: While Nest is small, it comes with rxjs bundled, which pushed the total install size to over 6mb for core and common combined. That's still small by any standard though.
  • COMMUNITY: massive. Development is very active in 2024-Q1: 7 releases, 8 Contributors, 466 Commits, the last release was less than a month ago. A very safe bet.
  • ECOSYSTEM: Nestjs has 41 official modules, probably the largest among the frameworks we'll look at. Which means that you'll find an official and supported solution for just about anything. You can also find around 6500 packages built for it, which is huge.
  • USAGE & INTEREST: They seem to be going strong, with 12.8% of developers using the framework, according to State of JS 2020 survey.

moiva.io


Preparations

Let's get going. The cheaty method I used before with Koa won't fly here, as this is completely different. However, Nest comes with an amazing feature, a command line interface that provides commands to generate all the types of components you need, with the basic boilerplate code. So, to create the app, all you have to do is generate the app itself and the 3 features we need (Users, Lists, Tasks).

As usual,

  • we use the todo-template to start a new todo-nest repo in GitHub,
  • clone it,
  • move the following files into temp folder, as Nest creates its own: package.json, tsconfig.json, tsconfig.build.json, README.md, .gitignore, .prettierrc, .eslintrc.js
  • install Nest.js (we won't need nodemon, as Nest comes with hot reload included):

npm install --global @nestjs/cli
npm install @nestjs/core @nestjs/common        

  • generate the app:

nest new todo-nest        

  • overwrite the generated config files with the ones from our temp directory, EXCEPT for the package.json, which needs to be merged. Just use the resulting one from the repo, as it is a bit complicated.
  • run npm install

Great, we now have a scaffolded Nest app.


Scaffolds

So, what's in there? Other than the config files, which we have overwritten for the most part, we have 2 folders, our usual src for source code, and test for end2end tests (that means, testing an entire route, with all the integrations). We're not interested in the tests right now, so let's see what's in the source code.

We have our entry point, main.ts, which simply creates the app (boot-straps it, in Nest lingo):

For easy testing, I'll change the default port to my favorite 3333. The most interesting thing here is that we create a Nest container that will hold the dependency tree (all the modules with their respective controllers, services etc). This points to AppModule, which lives in app.module.ts. It is here that we register all the feature modules.

We also have an app.service.ts, with a simple Hello World page, and a controller for that. We will delete those after we create our own routes. You'll also notice that it pre-generated test files, which run with Jest, a testing framework developed by Facebook. We also don't care about this for now.

We can start the app with 'npm run start', as usual. After you check it out, you can safely delete app.service.ts, app.controller.ts, app.controller.spec.ts.

Generating feature modules

It's just as easy to generate a component, there's a command 'nest generate <componentName> <name>'. There's a neat component called 'resource' that also generates endpoints, data transfer objects, and even entities. We will only need the endpoints for now. Also, we need a database service, with no controllers. So, let's do:

nest generate resource modules/user
nest generate resource modules/list
nest generate resource modules/task
nest generate service modules/database        

Bam! We have everything, modules are properly registered in the app.module, services, and controllers are correctly registered in their feature modules, and we even have some routes pre-generated! We will need to change the names and add logic, but it's already there:

A few caveats, as I discovered that my favorite settings from my original todo-template repo don't want to share the same bed with Nest, so I had to adapt them:

  • because TypeScript is stricter, Nest does not play well with my python-style code formatting, (no-semicolons, no optional curly brackets etc - .prettierrc),
  • it's not happy with the spec files being excluded from the build, as they need to be, as they are *.ts files and need to be transpiled into plain JavaScript to run with node. Therefore we need to have a separate tsconfig.build.json that extends the main one.
  • most of the scripts in my template package.json will not fly with TypeScript, so many changes there.


Modules

Nest has a steep learning curve, and one of the most misunderstood aspects is modules and DI containers. If I had to explain this to my grandma, I'd say Nest is like a Matryoshka. From the outside, you only see the AppModule doll, but when you open it, you'll find it contains 3 dolls, as it imports: [UserModule, ListModule, TaskModule]. But what's inside the UserModule, for instance?

Well, each has 3 more dolls inside, one controller, and 2 services. Our app has only 2 levels of nesting, but in larger projects, it's quite possible to go many more levels deeper.

Besides keeping track of modules, the main Matryoshka doll also keeps track of controllers and globally available providers:

When it imports the User, List, and Task modules, it also imports their controllers and services, and it is able to create a tree of all the controllers. In fact, when you start the app, you can see the routes mapped by all these controllers:

Each of these controllers and providers (services, database clients etc) contains a single class, which is instantiated as a singleton on application start. Every class that is decorated with @Injectable() class MyClass, can be injected anywhere with, you guessed it, @Inject(MyClass).

Decorators

Nest's solution to get rid of the callback hell, to inject dependencies, to apply middleware and to do basically anything you will need to do, is @Decorators. What are those? Decorators in NestJS are a fundamental part of its architecture, providing a way to enhance classes/properties/methods with additional functionality and metadata.

Controllers

So, how is routing handled? Well, to create a route all you have to do is to create a method for it (which, in our example, is just a wrapper for the service it calls), and decorate it with the HTTP verb decorator you need. For example:

The @Get() decorator takes the route as a parameter, so the RouterExplorer knows how to map this on the application bootstrap.

Later on, we'll do everything else we need to do at the controller lever (authentication, authorization, user input validation, input piping, and transformations, swagger documentation etc) with more decorator, added on top of the method.

You can see we're using another decorator, @Param('id) id: string. You guessed it, this is how you pass a parameter. If it was a Query param, you'd simply use @Query(), for Body params - @Body(). As simple as that. No need for req or ctx. (You can still access the res object with @Res() or @Response() if you really want to)

Providers

There are very few notable differences compared to our Express or Koa implementations, no point dwelling on these. The only thing worth mentioning is that I took advantage of the dependency injection system and added the database connection in the constructor:

  constructor(private readonly databaseService: DatabaseService) {}        

That's the preferred way, but you can also inject it as a property, outside the constructor:

  @Inject(DatabaseService) private readonly databaseService: DatabaseService;        


Concluding remarks

Because I've been working with Nest for a while now, it took me some 20 minutes to prepare and build the entire app. So, once you learn Nest, development is a breeze. Also, the Nest code is so much cleaner and easier to read. It does come at the cost of a steeper learning curve, but I believe it's worth the time.






要查看或添加评论,请登录

Teolin Codreanu的更多文章

社区洞察

其他会员也浏览了