At Quintessential we strive to deliver high quality, beautiful and stable products. Of course, this goes all the way to the back-end. We don't try to put a pretty face on a monster.
Up until now, all of our back-end code was written in JavaScript using the ExpressJS framework. We recently decided to give NestJS a try and it has actually been working really well for us.
So, first things first, I hear you asking “What is this NestJS you are talking about John? Why would I want to switch to it?”. It is a relatively new web framework that uses TypeScript with its own Dependency Injection system heavily inspired by Angular.
Before we dive into the pros and cons, let’s talk about the first thing, that I know popped into your head if you are currently using Express. The holy middleware! What will we do without all the juicy middleware available for Express that make our lives bearable? Don’t worry, NestJS is using Express and Socket.io (if you want to use sockets) under the hood by default, so everything will work! Now that we addressed the elephant in the room, let’s begin.
I am sure you all saw this one coming. Now, while TypeScript is not the unicorn that came to the js world to save us all, it is a really nice language that offers type safety and compile-time checking, which can help reduce a lot of errors. More importantly, it helps reduce many errors that happen at runtime. I’m sure we all hate to find out that the code broke because you wrote user.uername somewhere. Also, the types serve as self-documentation for your code and the data structures you use. This becomes especially important when your project is getting bigger and there are more people involved.
In Nest, you have controllers and services. The controllers usually just take the data needed from the request, call a service that does all the database calls and other operations, and return the value which is automatically sent as a response in JSON. The controllers, though, can also return a promise, in which case Nest will wait for it to be resolved and will return the value. Async/await arguably makes the code much more readable, but you should be careful when and if you should use await on a promise because it can silently make your code synchronous. Now you are probably asking “What if this async controller function throws an exception?”. This leads us to the next point.
As we previously said, the controllers can return a promise, so what if something in there goes haywire and throws an exception? Well, Nest has got you covered. It catches any exception and returns the appropriate response. This becomes even more powerful when you choose to throw an exception inside the controller (or in the service the controller calls). Nest has a custom Exception class called HttpException which takes an object or a string as a first argument and a status code a second argument. When this is thrown inside the controller, Nest catches it and sends the response message or the object with the status code you specified. So in case, let’s say a user does not exist you’d just have to write
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
and you’d be all set (the HttpStatus used above is just an enum provided by Nest with status codes).
Since Nest uses a Dependency Injection system like Angular’s, I think it is not a surprise that it supports and heavily encourages using modules. So, for example, you would have a users module, along with users service and users controller. This would do all the operations on the users, the controller will handle all endpoints under/users and you just have to import the users module in the app module to make all this work. This can really help make the API design easier and more “natural”.
“Oh come on John you can’t possibly be serious, you just listed TypeScript as the first pro, what is this?” you might be thinking. But hear me out on this one. While having type safety and static code checking at compile-time can be a great thing, it also introduces a lot of boilerplate code, and sometimes you might feel you are just writing the same thing all over again. The Nest CLI helps a lot with that, since it lets you generate modules, services, and controllers with all the boilerplate code needed from scratch, but there are other places that just can’t be helped. What do I mean? Let’s suppose you’re using mongoose, in order to use a User model you have to do this:
export interface UserInterface extends mongoose.Document {
username: string;
passwordHash: string;
accessRights: string;
createdAt: Date;
updatedAt: Date;
}
const userSchema = new Schema({
username: {
type: String,
required: true,
trim: true
},
passwordHash: {
type: String,
required: true
},
accessRights: {
type: String,
enum: ['user', 'admin'],
default: 'user'
}
},
{
timestamps: true
});
export const User: mongoose.Model<UserInterface> = mongoose.model('User', userSchema, 'Users');
Now, you should also use a DTO (data transfer object) to get let’s say the data from the request (not really necessary, but usually a good practice), which would look like this:
export interface User {
username: string;
password: string;
accessRights?: string;
}
So suppose you want to add another field to the user model, let’s say birthday. Now you have to edit three files in order to do that. While, this is actually not all bad since it makes refactoring relatively safer since if you forgot to add or remove a field somewhere, TypeScript will complain loudly, I have to admit that it gets quite cumbersome after a while.
Yes, NestJS works just fine with Express middleware, and writing your own is actually quite easy. How do you apply your own middleware though? It is explained quite well in the docs here: https://docs.nestjs.com/middleware, but consider the following cases. So you want to add your own global middleware for all controllers? Easy! Do you want to add a middleware for just a few controllers? Easy! You want to add a middleware for all the endpoints, but exclude just a couple(which is a quite common case especially with an auth middleware, where you want to exclude the login endpoint)? No can do. The way to do this is caveman style by checking inside the middleware if the request URL and method match the endpoint you want to exclude. Why is this? Well, you can’t both use the forRoutes method for all the paths and methods AND the exclude method for a path and method. It may be added in the future, but as of the time of writing this is the case and it actually bothers me a little :P
In conclusion, NestJS is definitely not the one-size-fits-all framework for the back-end and some of its quirks may make it unsuitable for your use case, but I think that it is definitely worth giving it a try especially if you are currently using Express and feel like having a type system or structured modularity, would help you and your team.
P.S If you liked this article, be sure to clap and I’ll be sure to make a second one when we have enough data, about the performance of our new product.
Don’t forget to follow our work on our social media and subscribe to our newsletter to be the first to get all new articles and info about our events.