Ep.08: Nest.js (Basic app for Node Backend Frameworks Comparison)
Teolin Codreanu
Software Developer | Microservices / APIs in Node.js, JavaScript, TypeScript, Nest.js / Koa / Express etc
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
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:
Nest calls itself a progressive framework. Now what in the name of the Holy Admin is that? Well, it basically means that it:
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.
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,
npm install --global @nestjs/cli
npm install @nestjs/core @nestjs/common
nest new todo-nest
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:
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.