Express vs NestJS: Which to choose in 2021

Express vs NestJS: Which to choose in 2021

ยท

6 min read

I know what you're thinking. When it comes to Node backend frameworks, Express is the undisputed king. Its un-opinionated and minimalistic approach means it's a super versatile tool for every use case.

If that's what you think, then of course, you'd be largely correct. I've used Express for a number of projects and it's been... fine. It's been plenty fast and easy enough to work with. The thing is, as the saying goes: "with great power comes great responsibility". In Express' case, this great power takes the form of having free rein over the architecture of your entire application. This may seem like a massive plus at the start, but as your app continues to grow, it can come back to bite you if you've not thought it through well enough at the start.

Enter NestJS

What is NestJS I hear you ask? It's a Node framework designed for building fast, reliable and (very importantly) scaleable applications. It brings a robust architecture to the table (based on Angular) and uses TypeScript by default (although you can use JS if you wish).

Perhaps what's more interesting about Nest, is what it doesn't do. It doesn't try to reinvent the wheel. Under the hood, it runs on Express (by default) or Fastify and provides a very clean level of abstraction on top of these frameworks whilst also allowing you utilise the platform specific APIs directly.

Nest is like Express with batteries included

Why NestJS

As you may have thought, that sounded rather interesting so I decided to take a look deeper into what it was all about.

Documentation

One of the things that blew me away when I first looked into Nest, was just how detailed and verbose their documentation is. They cover just about every topic you can imagine and provide detailed instructions on everything from database connections with TypeORM or Mongoose through to Authentication & Authorisation right through to websockets. If you can think it, you can almost bet it's in the documentation. They also allow you to convert all their code examples from TS to JS incase you decide you want to ditch the types.

Features

Nest is a very versatile framework and it has so many useful features. It has everything you'd expect from a backend framework such as the tools for building REST and GraphQL (using Apollo server) APIs. It can also be used for server rendered applications using a rendering engine such as Handlebars or EJS. Some of the more interesting features that is provides are a simple implementation of web sockets that uses Socket.IO server under the hood and has the ability to run cron-like tasks from inside your application. For a full list of what Nest can do, I highly recommend looking at their documentation as what I've mentioned doesn't scratch the surface.

Developer Experience

You may be thinking, this is all nothing new, Express can do all of these things and you'd be right! They're absolutely possible with Express. In fact, a lot of the packages that Nest uses for these extra features are built on existing Node packages that work with Express. In my opinion, where Nest stands out as different is when it comes to the developer experience. It's able to utilise all of these cool features, whilst also maintaining a consistent and testable architecture for your applications. This also has the added benefit of reducing decision fatigue and allowing you to switch between Nest projects easily as they all follow the same format.

Application Structure

Alright, alright I hear you say. This is all well and good, but what does a project actually look like? Great question! Let's take brief tour...

Modules

A module is a way of separating different features of your application. Think: Auth, Users, Pets etc. Each module can have child modules allowing you to split your application up per feature in a logical way. Each application has a single App module to wrap them all.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Controllers

The module's controller is what's responsible for handling incoming requests and responding to the client. In the example below, you can see we've got a GET request and a POST request. You can perform login in these controllers but for more complex or reusable logic, we pass that off to a service.

import { Body, Controller, Get, Post } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Post()
  addHello(@Body() body) {
    return this.appService.addHello(body);
  }
}

Services

A service is a type of Provider. A provider is a class that's annotated with the Injectable decorator. This allows it to be injected at runtime to the necessary controllers. This loose coupling is one of the foundation principles of Dependency Injection which we will talk about in a moment. For now, let's just note that services are for application logic, database interactions... that kinda stuff.

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {

  getHello(): string {
    return 'Hello World!';
  }

  addHello(body) {
    return `Hello ${body.name}`;
  }
}

Dependency Injection

Nest takes advantage of a pattern called Dependency Injection. If you're not familiar with the concept, you might find it useful to read about it here. The basic concept is that you decouple a class from the methods it requires (in our case a service which contains our methods). This allows the class to request the decencies from a DI container instead of us telling them explicitly where they are. Instead, it requests the service from this DI container based on the class' type. This means, you can pass in dummy versions of those methods or services that resolve dummy data for testing purposes. In order to get an instance of your service inside your controller, you need to make sure you list it in the providers array in the module:

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})

and then accept it in your controller's constructor:

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}
}

Conclusion

Okay, so that was quite a lot of information about Nest. I know for my projects, now that I've used Nest, I wouldn't go back to Express. This isn't because I dislike Express, it's because the list of things that Nest gives you out the box or by means of first party plugins is very long. The out-of-the-box consistent structure across your project, the dependency injection baked in and and the reduce in decision fatigue that comes from picking packages to use makes it easy to recommend. Yes, there is a little bit more of a learning curve to climb, but getting to the top will absolutely make you a 10x developer. ๐Ÿ˜œ

What are your thoughts? Have you used Nest? If so, what did you like or what did you dislike? Or are you a fan of Express or some other framework? I would love to hear from you!

ย